Files
descarga_factura_transporte/docs/procesar_xmls.md
T

8.5 KiB
Raw Blame History

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