Files
georgeandClaude Sonnet 5 15e3b5079c se agrega fallback OWTR y calculo de fecha de vencimiento por condicion de pago
- hana_manager.get_delivery ahora busca en ODLN y, si no encuentra, en OWTR
- sapb1_manager resuelve StockTransfers -> Orders (via U_IDC_NUMOV) para
  validar precios de flete cuando la guia es un traslado de inventario
- se corrige el orden de parametros tipo/serie/numero (sin cruces) en
  get_delivery, get_delivery_notes y _get_delivery_entries
- se agrega sapb1_manager.obtener_condicion_pago (BusinessPartners +
  PaymentTermsTypes) y pipeline._calcular_fecha_vencimiento para que
  DocDueDate del draft sea fecha de contabilizacion + dias de la condicion
  de pago del proveedor
- se agrega variante de regex para "VALOR REFER" truncado en sunat_schema.py
- se actualiza documentacion (docs/) reflejando estos cambios

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 13:21:04 -05:00

7.9 KiB

_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

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

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

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

# 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)