Files
descarga_factura_transporte/docs/procesar_xmls.md
T

258 lines
8.5 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 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"
}
]
```