Files
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

11 KiB
Raw Permalink Blame History

_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

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

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

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

# 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).