2026-09-02 07:40:26 -05:00
|
|
|
# _procesar_pdfs — Extracción de datos desde PDFs
|
|
|
|
|
|
|
|
|
|
## Ubicación
|
|
|
|
|
|
2026-09-10 13:21:04 -05:00
|
|
|
- **Archivo:** `pipeline.py` (línea 255-308, función anidada en `_procesar_un_correo`)
|
|
|
|
|
- **Librería:** `pdf_utilities.py` (línea 1-464)
|
2026-09-02 07:40:26 -05:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Flujo general
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
flowchart TD
|
|
|
|
|
A[_procesar_pdfs] --> B[Iterar adjuntos del email]
|
|
|
|
|
B --> C{Es .pdf?}
|
|
|
|
|
C -- No --> B
|
|
|
|
|
C -- Sí --> D[extraer_datos_pdf]:::highlight
|
|
|
|
|
D --> E{Hay datos sin error?}
|
|
|
|
|
E -- No --> F[⚠️ Log error]
|
|
|
|
|
F --> B
|
|
|
|
|
E -- Sí --> G[renombrar_pdf]:::highlight
|
|
|
|
|
G --> H[Actualizar email.adjuntos]
|
|
|
|
|
H --> I[Acumular en datos_pdf]
|
|
|
|
|
I --> J[Guardar en pdf_cache]
|
|
|
|
|
J --> B
|
|
|
|
|
|
|
|
|
|
B -- Fin --> K{datos_pdf vacío?}
|
|
|
|
|
K -- No --> L[email.datos_pdf = datos_pdf]
|
|
|
|
|
K -- Sí --> M[return pdf_cache]
|
|
|
|
|
L --> M
|
|
|
|
|
|
|
|
|
|
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Paso a paso
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
_procesar_pdfs() → dict (pdf_cache)
|
|
|
|
|
│
|
|
|
|
|
├── carpeta = _ruta_carpeta(download_folder)
|
|
|
|
|
│
|
|
|
|
|
├── Por cada adjunto en email["adjuntos"]:
|
|
|
|
|
│ │
|
|
|
|
|
│ ├── ¿No termina en ".pdf"? → continue
|
|
|
|
|
│ │
|
|
|
|
|
│ ├── pdf_path = carpeta + adjunto
|
|
|
|
|
│ │
|
|
|
|
|
│ ├── extraer_datos_pdf(pdf_path) → data
|
|
|
|
|
│ │ └── (ver detalle abajo)
|
|
|
|
|
│ │
|
|
|
|
|
│ ├── ¿data válido y sin error?
|
|
|
|
|
│ │ │
|
|
|
|
|
│ │ ├── renombrar_pdf(pdf_path, data) → nuevo_path
|
|
|
|
|
│ │ │ └── formato: {ruc}-{tipo}-{serie}-{numero}.pdf
|
|
|
|
|
│ │ │
|
|
|
|
|
│ │ ├── Si cambió el nombre:
|
|
|
|
|
│ │ │ ├── email["adjuntos"][idx] = nuevo_nombre
|
|
|
|
|
│ │ │ └── data["archivo"] = nuevo_nombre
|
|
|
|
|
│ │ │
|
|
|
|
|
│ │ ├── datos_pdf.append(data)
|
|
|
|
|
│ │ ├── pdf_cache[canonical] = data
|
|
|
|
|
│ │ └── 📄 PDF procesado: {canonical}
|
|
|
|
|
│ │
|
|
|
|
|
│ └── ¿Error? → ⚠️ PDF no procesado: {error}
|
|
|
|
|
│
|
|
|
|
|
├── Si hay datos → email["datos_pdf"] = datos_pdf
|
|
|
|
|
│
|
|
|
|
|
└── return pdf_cache
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## `extraer_datos_pdf(pdf_path)` `:highlight` — Detalle
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
flowchart TD
|
|
|
|
|
A[extraer_datos_pdf] --> B[pdfplumber.open]
|
|
|
|
|
B --> C[Extraer texto de todas las páginas]
|
|
|
|
|
C --> D[_extract_base_data]:::highlight
|
|
|
|
|
D --> E{Qué tipo de documento?}
|
|
|
|
|
|
|
|
|
|
E -- FACTURA --> F[_extract_factura_details]:::highlight
|
|
|
|
|
F --> G[_extract_detraccion_spatial]
|
|
|
|
|
|
|
|
|
|
E -- GUIA DE REMISION --> H[_extract_guia_remision_details]:::highlight
|
|
|
|
|
|
|
|
|
|
E -- GUIA DE REMISION TRANSPORTISTA --> I[_extract_guia_transportista_details]:::highlight
|
|
|
|
|
|
|
|
|
|
E -- BOLETA / otro --> J[return data]
|
|
|
|
|
|
|
|
|
|
G --> J
|
|
|
|
|
H --> J
|
|
|
|
|
I --> J
|
|
|
|
|
|
|
|
|
|
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `_extract_base_data(text, path)` — Datos base
|
|
|
|
|
|
|
|
|
|
Extrae del texto plano del PDF mediante regex:
|
|
|
|
|
|
|
|
|
|
| Campo | Fuente |
|
|
|
|
|
|-------|--------|
|
|
|
|
|
| `ruc_emisor` | `R.U.C. {11 dígitos}` o nombre del archivo o primer RUC en el texto |
|
|
|
|
|
| `tipo_documento` | "FACTURA ELECTRÓNICA" / "BOLETA DE VENTA" / "GUÍA DE REMISIÓN" / "GUÍA DE REMISIÓN TRANSPORTISTA" |
|
|
|
|
|
| `serie_numero` | `{4 caracteres} - {números}` ej: `F002-486` |
|
|
|
|
|
| `fecha_emision` | `dd/mm/yyyy` |
|
|
|
|
|
| `ruc_receptor` | Segundo RUC de 11 dígitos encontrado |
|
|
|
|
|
| `nombre_receptor` | Línea con `SEÑOR(ES) / CLIENTE / RAZÓN SOCIAL / ADQUIRIENTE` |
|
|
|
|
|
| `monto_total` | `TOTAL / IMPORTE TOTAL / NETO A PAGAR` |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### `_extract_factura_details(text, data)` `:highlight` — Factura
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
flowchart TD
|
|
|
|
|
A[_extract_factura_details] --> B[Buscar monto_total]
|
|
|
|
|
B --> C[Inicializar items[] y detraccion]
|
|
|
|
|
C --> D[Buscar cabecera de tabla]
|
|
|
|
|
D --> E[Parsear filas de items]
|
|
|
|
|
E --> F[Por cada item extraer:]
|
|
|
|
|
F --> G[guias_remision]
|
|
|
|
|
F --> H[guia_transportista]
|
|
|
|
|
F --> I[bolsas]
|
|
|
|
|
F --> J[peso / unidad_peso]
|
|
|
|
|
J --> K[Buscar detraccion]
|
|
|
|
|
K --> L[return]
|
|
|
|
|
|
|
|
|
|
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Items de tabla** (detecta ≥3 headers entre: CANT, DESCRIP, PRECIO, TOTAL, UNIT, VALOR, CÓDIGO, ITEM):
|
|
|
|
|
|
|
|
|
|
| Campo | Descripción |
|
|
|
|
|
|-------|-------------|
|
|
|
|
|
| `codigo` | Código alfanumérico del producto (opcional) |
|
|
|
|
|
| `cantidad` | Cantidad numérica |
|
|
|
|
|
| `descripcion` | Descripción del ítem |
|
|
|
|
|
| `precio_unitario` | Precio unitario |
|
|
|
|
|
| `total` | Total por línea |
|
|
|
|
|
|
|
|
|
|
**Por cada item**, parsea desde la descripción:
|
|
|
|
|
|
|
|
|
|
| Campo | Regex |
|
|
|
|
|
|-------|-------|
|
|
|
|
|
| `guias_remision[]` | `GRR / GR / G.R.R. / GUIA REMISION: {serie}-{num}` |
|
|
|
|
|
| `guia_transportista` | `GRT / G.R.T. / GUIA TRANSPORTISTA: {serie}-{num}` |
|
|
|
|
|
| `bolsas` | `{n} BOLSAS / BLS / SACOS` |
|
|
|
|
|
| `peso` / `unidad_peso` | `PESO: {valor} {TN/KG}` |
|
|
|
|
|
|
|
|
|
|
**Detracción**: Detecta si tiene detracción, porcentaje y monto.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### `_extract_guia_remision_details(text, data)` `:highlight` — Guía de Remisión
|
|
|
|
|
|
|
|
|
|
Extrae del texto:
|
|
|
|
|
|
|
|
|
|
| Campo | Descripción |
|
|
|
|
|
|-------|-------------|
|
|
|
|
|
| `peso_total` | Peso Total |
|
|
|
|
|
| `punto_partida` | Punto de Partida |
|
|
|
|
|
| `punto_llegada` | Punto de Llegada |
|
|
|
|
|
| `placa` | Placa del vehículo |
|
|
|
|
|
| `marca` | Marca del vehículo |
|
|
|
|
|
| `chofer` | Nombre del chofer |
|
|
|
|
|
| `brevete` | N° de brevete |
|
|
|
|
|
| `configuracion_vehicular` | Configuración vehicular |
|
|
|
|
|
| `items[]` | Lista de {cantidad, unidad, descripcion} |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### `_extract_guia_transportista_details(text, data)` `:highlight` — Guía de Remisión Transportista
|
|
|
|
|
|
|
|
|
|
Extrae del texto:
|
|
|
|
|
|
|
|
|
|
| Campo | Descripción |
|
|
|
|
|
|-------|-------------|
|
|
|
|
|
| `punto_partida` | Punto de Partida |
|
|
|
|
|
| `punto_llegada` | Punto de Llegada |
|
|
|
|
|
| `nombre_remitente` / `ruc_remitente` | Datos del remitente |
|
|
|
|
|
| `nombre_destinatario` / `ruc_destinatario` | Datos del destinatario |
|
|
|
|
|
| `documentos_relacionados[]` | Documentos relacionados |
|
|
|
|
|
| `peso_bruto_total_carga` | Peso bruto total |
|
|
|
|
|
| `unidad_medida_peso_bruto` | Unidad de medida |
|
|
|
|
|
| `indicador_pagador_flete` | Indicador del pagador |
|
|
|
|
|
| `nombre_pagador` / `ruc_pagador` | Datos del pagador |
|
|
|
|
|
| `datos_vehiculos[]` | Lista de {placa} |
|
|
|
|
|
| `datos_conductores[]` | Lista de {nombre, brevete} |
|
|
|
|
|
| `observaciones` | Observaciones |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## `renombrar_pdf(pdf_path, data)` `:highlight`
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
renombrar_pdf(pdf_path, data) → nuevo_path
|
|
|
|
|
│
|
|
|
|
|
└── generar_nombre_pdf(data) → nombre canónico
|
|
|
|
|
│
|
|
|
|
|
├── ruc = data["ruc_emisor"]
|
|
|
|
|
├── abrev = TIPO_MAP[data["tipo_documento"]]
|
|
|
|
|
│ └── FACTURA → "01", BOLETA → "03",
|
|
|
|
|
│ GUIA DE REMISION → "09", GUIA DE REMISION TRANSPORTISTA → "31"
|
|
|
|
|
├── serie, numero = data["serie_numero"].split("-")
|
|
|
|
|
│ └── numero = numero.lstrip("0") → elimina ceros a la izquierda
|
|
|
|
|
│
|
|
|
|
|
└── "{ruc}-{abrev}-{serie}-{numero}.pdf"
|
|
|
|
|
ej: "20537929520-01-F002-486.pdf"
|
|
|
|
|
|
|
|
|
|
└── os.rename(origen, destino)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Tipos de documento detectados
|
|
|
|
|
|
|
|
|
|
| Tipo detectado | Código | Sección extraída |
|
|
|
|
|
|----------------|--------|------------------|
|
|
|
|
|
| FACTURA ELECTRÓNICA | 01 | Items, detracción, guías de remisión |
|
|
|
|
|
| BOLETA DE VENTA | 03 | Solo datos base |
|
|
|
|
|
| GUÍA DE REMISIÓN | 09 | Peso, destino, vehículo, items |
|
|
|
|
|
| GUÍA DE REMISIÓN TRANSPORTISTA | 31 | Remitente, destinatario, vehículos, conductores |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Salida
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# pdf_cache → dict
|
|
|
|
|
{
|
|
|
|
|
"20537929520-01-F002-486.pdf": {
|
|
|
|
|
"archivo": "20537929520-01-F002-486.pdf",
|
|
|
|
|
"ruc_emisor": "20537929520",
|
|
|
|
|
"tipo_documento": "FACTURA",
|
|
|
|
|
"serie_numero": "F002-486",
|
|
|
|
|
"fecha_emision": "08/07/2026",
|
|
|
|
|
"ruc_receptor": "20123456789",
|
|
|
|
|
"nombre_receptor": "EMPRESA SAC",
|
|
|
|
|
"monto_total": "4680.00",
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"cantidad": "350",
|
|
|
|
|
"descripcion": "FLETE T001-127773 PESO: 23.26 TN GRR: T001-127773",
|
|
|
|
|
"precio_unitario": "1.3396",
|
|
|
|
|
"total": "468.86",
|
|
|
|
|
"guias_remision": ["T001-127773"],
|
|
|
|
|
"guia_transportista": None,
|
|
|
|
|
"bolsas": None,
|
|
|
|
|
"peso": "23.26",
|
|
|
|
|
"unidad_peso": "TN"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"detraccion": {"tiene": True, "porcentaje": "4.00", "monto": "187.20"}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# email["datos_pdf"] → list[dict] (misma estructura)
|
|
|
|
|
```
|