Files
GeorgeandClaude Sonnet 5.5 6030320822 se actualiza la documentación: sapb1_manager, nombres de funciones y cifras
- 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>
2026-10-22 14:56:34 -05:00

280 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).