# _procesar_pdfs — Extracción de datos desde PDFs ## Ubicación - **Archivo:** `pipeline.py` (línea 193-246) - **Librería:** `pdf_utilities.py` (línea 1-450) --- ## 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) ```