Files
descarga_factura_transporte/docs/procesar_xmls.md
T
georgeandClaude Sonnet 5 15e3b5079c se agrega fallback OWTR y calculo de fecha de vencimiento por condicion de pago
- 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>
2026-09-10 13:21:04 -05:00

280 lines
11 KiB
Markdown
Raw 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:** `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`).