- 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>
11 KiB
_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).