8.5 KiB
8.5 KiB
_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
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
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
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
# 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"
}
]