- 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>
7.9 KiB
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)