257 lines
8.5 KiB
Markdown
257 lines
8.5 KiB
Markdown
# _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"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|