Files

262 lines
7.9 KiB
Markdown
Raw Permalink Normal View History

# _procesar_pdfs — Extracción de datos desde PDFs
## Ubicación
- **Archivo:** `pipeline.py` (línea 255-308, función anidada en `_procesar_un_correo`)
- **Librería:** `pdf_utilities.py` (línea 1-464)
---
## 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)
```