# _procesar_xmls — Extracción y cruce de datos desde XML ## Ubicación - **Archivo:** `pipeline.py` (línea 310-463, función anidada en `_procesar_un_correo`) - **Librería:** `sunat_schema.py` (línea 1-720) --- ## 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í --> CDR{Es CDR?} CDR -- Sí --> CDR2[Solo renombrar, continue] CDR2 --> B CDR -- No --> G[Renombrar XML a nombre canónico] G --> H[Construir nombre PDF canónico y buscarlo] H --> RED[_copiar_a_red]:::highlight RED --> I[Cruzar documentos_referencia desde PDF si faltan]:::highlight I --> J[Asignar guías de remisión por línea]:::highlight J --> K[Agregar a lista de documentos] K --> B B -- Fin --> L[Post-proceso: resolver GRE desde guía transportista referenciada]:::highlight L --> M[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 │ │ └── Procesa Invoice tipo 01, DespatchAdvice tipo 09 y 31, y CDR (ApplicationResponse) │ │ │ ├── ¿data es None? → ℹ️ XML sin datos, continue │ │ │ ├── ¿data["es_cdr"]? → solo renombrar a "CDR-{ruc}-{tipo}-{serie}-{numero}.xml", continue │ │ │ ├── data["ruta_xml"] = xml_path │ │ │ ├── Renombrar XML a nombre canónico: │ │ └── "{ruc_emisor}-{tipo}-{serie}-{numero}.xml" │ │ │ ├── Construir nombre PDF canónico y buscar el PDF asociado: │ │ ├── 1. Mismo nombre canónico con extensión .pdf │ │ └── 2. Fallback: mismo nombre que el XML renombrado, con extensión .pdf │ │ └── data["ruta_pdf"] = ruta encontrada o None │ │ │ ├── _copiar_a_red(data, _log) │ │ └── Copia PDF/XML a la ruta de red (RUTA_RED_FACTURACION), si está configurada. │ │ Guarda data["ruta_red_pdf"] / data["ruta_red_xml"] (ver docs/pipeline.md). │ │ │ ├── ¿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 y transportista por línea (ver prioridad abajo) │ │ │ └── ✅ Agregar data a documentos[] │ ├── Post-procesamiento — resolver GRE desde guía de transporte referenciada: │ ├── guia_lookup = {(tipo, serie, numero): doc} para cada documento tipo 09/31 del correo │ └── Por cada factura (tipo 01), por cada documento_referencia tipo 31: │ ├── Buscar la guía transportista en guia_lookup │ ├── Tomar sus documentos_referencia tipo 09 (GRE) propios │ └── Asignarlos a las líneas de la factura que aún no tengan guias_remision │ └── 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 -- ApplicationResponse --> CDR[_parse_application_response]:::highlight CDR --> I2[return dict con es_cdr=True] C -- Invoice --> D{tipo == 01?} D -- Sí --> E[_parse_invoice]:::highlight D -- No --> F[return None] C -- DespatchAdvice --> G{tipo en 09, 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; ``` Procesa **Facturas tipo 01**, **Guías de Remisión Remitente tipo 09**, **Guías de Remisión Transportista tipo 31**, y **CDR** (`ApplicationResponse`, solo para renombrar el archivo, no se agrega a `documentos[]`). --- ### `_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` | | `guias_remision[]` / `guia_transportista` | Parseadas desde `cbc:Note` de la línea (`extraer_guias_desde_notes`) — ver prioridad de asignación más abajo | | `valor_referencial` | Extraído de la descripción del ítem, del `Note` de la línea, o de los `Note` a nivel de documento (`extraer_valor_referencial`) | --- ### `_parse_despatch_advice(root)` — Guías tipo 09 y 31 La misma función procesa tanto la Guía de Remisión Remitente (09) como la Guía de Remisión Transportista (31); el `tipo` viene de `DespatchAdviceTypeCode`. | 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` (para la guía transportista, típicamente referencian la GRE — guía remitente — asociada). --- ## 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 (dentro de `_procesar_xmls`) Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea de la factura, en el orden en que realmente se aplican en el código: | Prioridad | Origen | Descripción | |-----------|--------|-------------| | 1ª | `notes_text` (ya resuelto en `sunat_schema._parse_invoice`) | Si la línea ya trae `guias_remision`/`guia_transportista` extraídos del `cbc:Note` del XML (vía `extraer_guias_desde_notes`), se conservan tal cual. | | 2ª | `documentos_referencia` (`refs_09`/`refs_31`) | Si la línea no trajo guías desde `notes_text`, se asigna posicionalmente desde `documentos_referencia` (`refs_09[i]`, `refs_31[i]`; la última línea recibe todas las refs restantes). | ### 3. Post-procesamiento — resolver GRE desde guía de transporte referenciada Después de procesar todos los XML del correo, si una factura referencia una **guía transportista (31)** y esa guía (presente como documento independiente en el mismo correo) referencia a su vez una o más **GRE (09)**, esas GRE se propagan a las líneas de la factura que aún no tengan `guias_remision` asignadas. ### 4. `_completar_guias` (paso posterior, fuera de `_procesar_xmls`) Si tras los pasos anteriores alguna línea sigue sin `guias_remision`, `_completar_guias` (ver `docs/pipeline.md`) las copia desde los `items[]` del PDF cache, parseando el texto con regex (`{T/V/E}{serie}-{numero}`). --- ## 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, "valor_referencial": 468.86 } ], "ruta_xml": "/path/to/20537929520-01-F002-486.xml", "ruta_pdf": "/path/to/20537929520-01-F002-486.pdf", "ruta_red_pdf": "\\\\192.168.9.66\\b1_shf\\Facturacion Electronica\\PRODUCCION\\2026\\7\\8\\01\\20100029741\\20537929520\\20100029741-01-F002-486_FormatoImpreso.pdf", "ruta_red_xml": "\\\\192.168.9.66\\b1_shf\\Facturacion Electronica\\PRODUCCION\\2026\\7\\8\\01\\20100029741\\20537929520\\20100029741-01-F002-486.xml", "tipo_descripcion": "Factura" } ] ``` `ruta_red_pdf`/`ruta_red_xml` solo aparecen si `RUTA_RED_FACTURACION` está configurada en `.env` y la copia fue exitosa; se usan luego en `_build_sap_draft` para llenar `U_VS_ANEXO_PDF`/`U_VS_ANEXO_XML` (ver `docs/pipeline.md`).