- Nueva página docs/sapb1_manager.md: sesión, funciones, clasificación de errores (SapNoDisponible frente a rechazo de SAP), búsqueda de facturas existentes y cómo se ubica una guía. - procesar_pdfs.md y procesar_xmls.md usaban los nombres anteriores a la división de pipeline.py (_procesar_pdfs, _ruta_carpeta...) y números de línea que envejecen; ahora apuntan a documentos.py. - inventario.md: árbol de dependencias con los módulos nuevos. - pipeline.md: _procesar_factura, importes_coinciden, hana_disponible y buscar_factura_existente; validar_precios.md, hana_manager.md, main.md y procesar_xmls.md con las notas que faltaban; README sin la cifra de pruebas vieja. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
280 lines
11 KiB
Markdown
280 lines
11 KiB
Markdown
# procesar_xmls — Extracción y cruce de datos desde XML
|
||
|
||
## Ubicación
|
||
|
||
- **Archivo:** `documentos.py`, función `procesar_xmls(email, download_folder, pdf_cache)`; la llama `pipeline._procesar_un_correo`
|
||
- **Librería:** `sunat_schema.py`
|
||
|
||
---
|
||
|
||
## 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(email, download_folder, 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)
|
||
│ │ └── 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`, es una ruta absoluta en el sistema operativo (una ruta UNC fuera de Windows no lo es: no se copia) y la copia fue exitosa; se usan luego en `draft_sap.build_sap_draft` para llenar `U_VS_ANEXO_PDF`/`U_VS_ANEXO_XML` (ver `docs/pipeline.md`).
|