# _procesar_xmls — Extracción y cruce de datos desde XML ## Ubicación - **Archivo:** `pipeline.py` (línea 248-370) - **Librería:** `sunat_schema.py` (línea 1-520) --- ## Flujo general ```mermaid flowchart TD A[_procesar_xmls] --> B[Iterar adjuntos del email] B --> C{Es .xml?} C -- No --> B C -- Sí --> D[extraer_datos_xml]:::highlight D --> E{Hay datos?} E -- No --> F[ℹ️ XML sin datos] F --> B E -- Sí --> G[Construir nombre PDF canónico] G --> H[Buscar PDF en cache o fallback] H --> I[Cruzar documentos_referencia desde PDF]:::highlight I --> J[Cruzar guías de remisión por línea]:::highlight J --> K[Agregar a lista de documentos] K --> B B -- Fin --> L[return documentos] classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px; ``` --- ## Paso a paso ``` _procesar_xmls(pdf_cache) → list[dict] │ ├── carpeta = _ruta_carpeta(download_folder) │ ├── Por cada adjunto en email["adjuntos"]: │ │ │ ├── ¿No termina en ".xml"? → continue │ │ │ ├── xml_path = carpeta + adjunto │ │ │ ├── sunat_schema.extraer_datos_xml(xml_path) → data | None │ │ └── Solo procesa Invoice tipo 01 y DespatchAdvice tipo 31 │ │ │ ├── ¿data es None? → ℹ️ XML sin datos, continue │ │ │ ├── data["ruta_xml"] = xml_path │ │ │ ├── Construir nombre PDF canónico: │ │ └── "{ruc_emisor}-{tipo}-{serie}-{numero}.pdf" │ │ │ ├── Buscar PDF asociado: │ │ ├── 1. pdf_cache por nombre canónico │ │ └── 2. Fallback: mismo nombre con extensión .pdf │ │ └── data["ruta_pdf"] = ruta encontrada o None │ │ │ ├── ¿data sin documentos_referencia? │ │ ├── Buscar en pdf_cache por nombre canónico │ │ │ └── pdf_utilities.extraer_referencias(items) → refs │ │ ├── Si no → fallback: buscar en email["datos_pdf"] │ │ └── Si encuentra → data["documentos_referencia"] = refs │ │ │ ├── Asignar guías de remisión por línea: │ │ ├── refs_09 = [tipo 09] desde documentos_referencia │ │ ├── refs_31 = [tipo 31] desde documentos_referencia │ │ ├── pdf_items = pdf_cache[canonical].items[] (si existe) │ │ │ │ │ └── Por cada línea en data["lineas"]: │ │ │ │ │ ├── ¿Hay PDF items para esta línea? │ │ │ ├── Parsear guias_remision[] desde descripción PDF │ │ │ │ └── Regex: "{serie}-{numero}" con prefijo T/V/E │ │ │ └── Parsear guia_transportista desde descripción PDF │ │ │ │ │ ├── ¿Hay refs_09 para esta línea? │ │ │ └── Asignar desde documentos_referencia ordenados │ │ │ │ │ └── No → guias_remision = [], guia_transportista = None │ │ │ └── ✅ Agregar data a documentos[] │ └── return documentos ``` --- ## `extraer_datos_xml(xml_path)` `:highlight` ### Flujo de decisión ```mermaid flowchart TD A[extraer_datos_xml] --> B[ET.parse xml] B --> C{Tag raíz?} C -- Invoice --> D{tipo == 01?} D -- Sí --> E[_parse_invoice]:::highlight D -- No --> F[return None] C -- DespatchAdvice --> G{tipo == 31?} G -- Sí --> H[_parse_despatch_advice]:::highlight G -- No --> F C -- Otro --> F E --> I[return datos.to_dict] H --> I classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px; ``` Solo procesa **Facturas tipo 01** y **Guías de Remisión Transportista tipo 31**. --- ### `_parse_invoice(root)` — Factura tipo 01 ```mermaid flowchart TD A[_parse_invoice] --> B[ID → serie, numero] B --> C[IssueDate → fecha_emision] C --> D[InvoiceTypeCode → tipo] D --> E[AccountingSupplierParty → ruc_emisor, razon_social] E --> F[AccountingCustomerParty → ruc_receptor, razon_social] F --> G[LegalMonetaryTotal → total_factura] G --> H[DespatchDocumentReference → documentos_referencia] H --> I{¿Hay refs?} I -- No --> J[Buscar en descripción de 1ra línea] I -- Sí --> K{Solo si tipo == 01} J --> K K --> L[PaymentTerms → detracción] L --> M[InvoiceLine → líneas de detalle] M --> N[return DatosComprobante] classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px; ``` **Documentos de referencia** se obtienen de dos formas: | Origen | Cómo | |--------|------| | `DespatchDocumentReference` | Tags XML estándar SUNAT (`cbc:ID`, `cbc:DocumentTypeCode`) | | Fallback en descripción | Si no hay refs, busca en `cbc:Description` de la primera línea con regex `{serie}-{numero}` | **Detracción** se extrae de `PaymentTerms` con `ID = "Detraccion"`: | Campo | Tag | |-------|-----| | `codigo_detraccion` | `cbc:PaymentMeansID` | | `porcentaje_detraccion` | `cbc:PaymentPercent` | | `monto_detraccion` | `cbc:Amount` | **Líneas de detalle** (`InvoiceLine`): | Campo | Tag | |-------|-----| | `descripcion` | `cac:Item/cbc:Description` | | `cantidad` | `cbc:InvoicedQuantity` | | `precio_unitario` | `cac:PricingReference/cac:AlternativeConditionPrice/cbc:PriceAmount` o `cac:Price/cbc:PriceAmount` | | `total_linea` | `cbc:LineExtensionAmount` | --- ### `_parse_despatch_advice(root)` — Guía tipo 31 | Campo | Tag | |-------|-----| | `ruc_emisor` | `DespatchSupplierParty/Party/PartyIdentification/ID` | | `razon_social_emisor` | `PartyName/Name` o `PartyLegalEntity/RegistrationName` | | `ruc_receptor` | `DeliveryCustomerParty/Party/PartyIdentification/ID` | | `razon_social_receptor` | `PartyLegalEntity/RegistrationName` | | `punto_partida` | `Delivery/Despatch/DespatchAddress/AddressLine/Line` | | `punto_llegada` | `Delivery/DeliveryAddress/AddressLine/Line` | | `peso_bruto` | `GrossWeightMeasure` | | `unidad_peso_bruto` | `GrossWeightMeasure` (atributo `unitCode`) | | `fecha_traslado` | `ShipmentStage/TransitPeriod/StartDate` | | `vehiculo_principal` | `TransportHandlingUnit/TransportEquipment/ID` | | `vehiculo_secundario` | `TransportHandlingUnit/TransportEquipment/AttachedTransportEquipment/ID` | | `licencia_conductor` | `ShipmentStage/DriverPerson/IdentityDocumentReference/ID` | | `nombre_conductor` | `ShipmentStage/DriverPerson/FirstName` | | `documento_conductor` | `ShipmentStage/DriverPerson/ID` | | `ubigeo_partida` | `Delivery/Despatch/DespatchAddress/ID` | | `lineas[]` | `DespatchLine` con `DeliveredQuantity` y `Item/Description` | Documentos de referencia desde `AdditionalDocumentReference`. --- ## Cruce de referencias `:highlight` ### 1. Completar `documentos_referencia` desde PDF Si el XML no trae documentos de referencia, se buscan en el PDF cache: ``` pdf_cache[nombre_canonico].items[] → extraer_referencias() └── Por cada item: ├── guias_remision[] → ref tipo 09 └── guia_transportista → ref tipo 31 ``` ### 2. Asignar guías por línea Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea: | Prioridad | Origen | Descripción | |-----------|--------|-------------| | 1ª | PDF items | Parse desde `items[i].guias_remision[]` y `items[i].guia_transportista` con regex `{T/V/E}{serie}-{numero}` | | 2ª | `documentos_referencia` | Asignación ordinal: `refs_09[i]`, `refs_31[i]` | | 3ª | Default | `[]` y `None` | --- ## Datos que produce ```python # documentos → list[dict] [ { "ruc_emisor": "20537929520", "razon_social_emisor": "TRANSPORTES S.A.C.", "fecha_emision": "2026-07-08", "tipo": "01", "serie": "F002", "numero": "486", "ruc_receptor": "20123456789", "razon_social_receptor": "CLIENTE S.A.", "total_factura": 4680.0, "tiene_detraccion": True, "codigo_detraccion": "014", "porcentaje_detraccion": 4.0, "monto_detraccion": 187.20, "documentos_referencia": [ {"tipo": "09", "serie": "T001", "numero": "127773"} ], "lineas": [ { "descripcion": "FLETE T001-127773", "cantidad": 350.0, "precio_unitario": 1.3396, "total_linea": 468.86, "guias_remision": [{"tipo": "09", "serie": "T001", "numero": "127773"}], "guia_transportista": None } ], "ruta_xml": "/path/to/20537929520-01-F002-486.xml", "ruta_pdf": "/path/to/20537929520-01-F002-486.pdf", "tipo_descripcion": "Factura" } ] ```