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>
This commit is contained in:
1 parent
6b0ed6f519
commit
15e3b5079c
231 files changed
+21421
-475
No files matched your search
+128
-11
@@ -12,6 +12,8 @@ import os
|
||||
import re
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import ntpath
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import email_utilities
|
||||
@@ -22,6 +24,9 @@ import sapb1_manager
|
||||
import pdf_utilities
|
||||
import hana_manager
|
||||
|
||||
RUTA_RED_FACTURACION = os.getenv("RUTA_RED_FACTURACION", "")
|
||||
RUC_EMPRESA = os.getenv("RUC_EMPRESA", "")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HELPERS
|
||||
@@ -30,6 +35,75 @@ def _ruta_carpeta(subcarpeta: str) -> str:
|
||||
return os.path.join(os.path.dirname(__file__), subcarpeta)
|
||||
|
||||
|
||||
def _copiar_a_red(data: dict, _log) -> None:
|
||||
"""
|
||||
Copia el PDF y el XML de un comprobante ya procesado a la ruta de red
|
||||
de Facturación Electrónica, organizada como::
|
||||
|
||||
{RUTA_RED_FACTURACION}\\{año}\\{mes}\\{día}\\{tipo}\\{RUC_EMPRESA}\\{ruc_emisor}\\
|
||||
{RUC_EMPRESA}-{tipo}-{serie}-{numero}_FormatoImpreso.pdf
|
||||
{RUC_EMPRESA}-{tipo}-{serie}-{numero}.xml
|
||||
|
||||
``RUC_EMPRESA`` es el RUC de Aberio (fijo, por ahora); ``ruc_emisor``
|
||||
es el RUC del proveedor que emitió la factura (varía por comprobante).
|
||||
Año/mes/día se toman de ``fecha_emision`` (mes y día sin ceros a la
|
||||
izquierda). No falla el pipeline si la ruta de red no está configurada
|
||||
o no está disponible: solo registra una advertencia.
|
||||
|
||||
Guarda las rutas resultantes en ``data["ruta_red_pdf"]`` y
|
||||
``data["ruta_red_xml"]`` para que ``_build_sap_draft`` las incluya en
|
||||
el draft (``U_VS_ANEXO_PDF`` / ``U_VS_ANEXO_XML``).
|
||||
"""
|
||||
if not RUTA_RED_FACTURACION:
|
||||
return
|
||||
|
||||
ruc_emisor = data.get("ruc_emisor", "")
|
||||
tipo = data.get("tipo", "")
|
||||
serie = data.get("serie", "")
|
||||
numero = data.get("numero", "")
|
||||
xml_path = data.get("ruta_xml")
|
||||
pdf_path = data.get("ruta_pdf")
|
||||
|
||||
try:
|
||||
fecha = datetime.strptime(data.get("fecha_emision", ""), "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
_log(f"⚠️ No se pudo copiar a red: fecha_emision inválida ({data.get('fecha_emision')})", 3)
|
||||
return
|
||||
|
||||
carpeta_destino = os.path.join(
|
||||
RUTA_RED_FACTURACION,
|
||||
str(fecha.year), str(fecha.month), str(fecha.day),
|
||||
tipo, RUC_EMPRESA, ruc_emisor,
|
||||
)
|
||||
# Ruta guardada en el draft SAP (U_VS_ANEXO_PDF/XML): siempre estilo
|
||||
# Windows/UNC con backslash, sin importar el separador del SO donde
|
||||
# corre el script (evita mezclar "\\" y "/" al usar os.path.join en
|
||||
# un host no-Windows).
|
||||
carpeta_destino_unc = ntpath.join(
|
||||
RUTA_RED_FACTURACION,
|
||||
str(fecha.year), str(fecha.month), str(fecha.day),
|
||||
tipo, RUC_EMPRESA, ruc_emisor,
|
||||
)
|
||||
nombre_base = f"{RUC_EMPRESA}-{tipo}-{serie}-{numero}"
|
||||
|
||||
try:
|
||||
os.makedirs(carpeta_destino, exist_ok=True)
|
||||
|
||||
if pdf_path and os.path.exists(pdf_path):
|
||||
ruta_red_pdf = os.path.join(carpeta_destino, f"{nombre_base}_FormatoImpreso.pdf")
|
||||
shutil.copy2(pdf_path, ruta_red_pdf)
|
||||
data["ruta_red_pdf"] = ntpath.join(carpeta_destino_unc, f"{nombre_base}_FormatoImpreso.pdf")
|
||||
|
||||
if xml_path and os.path.exists(xml_path):
|
||||
ruta_red_xml = os.path.join(carpeta_destino, f"{nombre_base}.xml")
|
||||
shutil.copy2(xml_path, ruta_red_xml)
|
||||
data["ruta_red_xml"] = ntpath.join(carpeta_destino_unc, f"{nombre_base}.xml")
|
||||
|
||||
_log(f"📁 Copiado a red: {carpeta_destino}", 3)
|
||||
except OSError as e:
|
||||
_log(f"⚠️ Error copiando a ruta de red: {e}", 3)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PASO 1: Autenticación Microsoft Graph
|
||||
# ============================================================================
|
||||
@@ -327,6 +401,8 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
pdf_path = pdf_path if os.path.exists(pdf_path) else None
|
||||
data["ruta_pdf"] = pdf_path
|
||||
|
||||
_copiar_a_red(data, _log)
|
||||
|
||||
# Cruzar documentos_referencia desde el PDF renombrado (pdf_cache por nombre canónico)
|
||||
if not data.get("documentos_referencia"):
|
||||
pdf_data = pdf_cache.get(pdf_canonical)
|
||||
@@ -520,7 +596,7 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
continue
|
||||
doc = {
|
||||
"ruc_emisor": raw.get("ruc_emisor"),
|
||||
"razon_social_emisor": raw.get("nombre_receptor"),
|
||||
"razon_social_emisor": None, # el PDF no extrae la razón social del emisor
|
||||
"fecha_emision": raw.get("fecha_emision"),
|
||||
"tipo": tipo,
|
||||
"serie": serie_p,
|
||||
@@ -586,6 +662,35 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
|
||||
return saved
|
||||
|
||||
def _calcular_fecha_vencimiento(fecha_base: str, condicion_pago: dict | None) -> str:
|
||||
"""
|
||||
Calcula la fecha de vencimiento sumando a ``fecha_base`` los días de
|
||||
la condición de pago (PaymentTermsTypes) del proveedor.
|
||||
|
||||
Usa ``NumberOfAdditionalDays`` como días a sumar; si ese valor es 0,
|
||||
usa ``NumberOfAdditionalMonths * 30`` en su lugar.
|
||||
|
||||
Args:
|
||||
fecha_base: Fecha de contabilización, formato "YYYY-MM-DD".
|
||||
condicion_pago: Resultado de ``sapb1_manager.obtener_condicion_pago``
|
||||
(campos ``NumberOfAdditionalDays``, ``NumberOfAdditionalMonths``),
|
||||
o None si el proveedor no tiene condición de pago configurada.
|
||||
|
||||
Returns:
|
||||
str — Fecha de vencimiento, formato "YYYY-MM-DD". Si
|
||||
``condicion_pago`` es None, retorna ``fecha_base`` sin cambios.
|
||||
"""
|
||||
fecha = datetime.strptime(fecha_base, "%Y-%m-%d")
|
||||
if not condicion_pago:
|
||||
return fecha.strftime("%Y-%m-%d")
|
||||
|
||||
dias = int(condicion_pago.get("NumberOfAdditionalDays") or 0)
|
||||
if dias == 0:
|
||||
dias = int(condicion_pago.get("NumberOfAdditionalMonths") or 0) * 30
|
||||
fecha += timedelta(days=dias)
|
||||
|
||||
return fecha.strftime("%Y-%m-%d")
|
||||
|
||||
def _build_sap_draft(factura: dict, resultados_val: list[dict]) -> dict:
|
||||
"""
|
||||
Construye el payload JSON para crear un Draft en SAP B1 Service Layer
|
||||
@@ -654,10 +759,15 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
ruc_emisor = factura.get("ruc_emisor", "")
|
||||
razon_social_emisor = factura.get("razon_social_emisor", "")
|
||||
|
||||
card_code = f"PL{ruc_emisor}"
|
||||
|
||||
fecha_contabilizacion = datetime.now().strftime("%Y-%m-%d")
|
||||
emision = datetime.strptime(fecha_emision, "%Y-%m-%d")
|
||||
item_code = "C103002-A" if (emision.year, emision.month) < (datetime.now().year, datetime.now().month) else "C103002"
|
||||
|
||||
condicion_pago = sapb1_manager.obtener_condicion_pago(card_code)
|
||||
fecha_vencimiento = _calcular_fecha_vencimiento(fecha_contabilizacion, condicion_pago)
|
||||
|
||||
|
||||
if tiene_detraccion:
|
||||
# neto = round(total - monto_detraccion, 2)
|
||||
@@ -713,9 +823,9 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
return {
|
||||
"DocType": "dDocument_Items",
|
||||
"DocDate": fecha_contabilizacion,
|
||||
"DocDueDate": fecha_emision,
|
||||
"DocDueDate": fecha_vencimiento,
|
||||
"TaxDate": fecha_emision,
|
||||
"CardCode": f"PL{ruc_emisor}",
|
||||
"CardCode": card_code,
|
||||
"NumAtCard": f"{tipo}{serie}-{numero}",
|
||||
"U_IDC_NUMGR": "" if not guia_referencia else guia_referencia,
|
||||
"DocCurrency": "SOL",
|
||||
@@ -737,6 +847,8 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
"U_IDC_FLETE": "02",
|
||||
"U_IDC_LINEANE": "01",
|
||||
"U_IDC_VALREF": valor_referencial,
|
||||
"U_VS_ANEXO_PDF": factura.get("ruta_red_pdf", ""),
|
||||
"U_VS_ANEXO_XML": factura.get("ruta_red_xml", ""),
|
||||
"DocumentLines": doc_lines,
|
||||
"DocumentInstallments": installments,
|
||||
}
|
||||
@@ -830,7 +942,7 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
codigo_guia = f"{guia['tipo']}{guia['serie']}-{guia['numero']}"
|
||||
try:
|
||||
delivery_notes = sapb1_manager.get_delivery_notes(
|
||||
tipo=guia["serie"], serie=guia["tipo"], numero=guia["numero"]
|
||||
tipo=guia["tipo"], serie=guia["serie"], numero=guia["numero"]
|
||||
)
|
||||
|
||||
wh_set = set()
|
||||
@@ -1175,14 +1287,19 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
||||
_log(f"⏭️ Draft NO creado: {serie}-{numero} — validación falló")
|
||||
html_body = _build_notification_body("skipped", factura, validation_table, valor_referencial_table)
|
||||
|
||||
# 16. Enviar notificación
|
||||
# 16. Enviar notificación al remitente original, con copia al equipo
|
||||
if token:
|
||||
outlook_utilities.send_email(
|
||||
token, email_addr,
|
||||
["jlopez@aberio.com.pe", "gcalle@pucp.pe"],
|
||||
f"Draft SAP B1 - {serie}-{numero}",
|
||||
html_body, adjuntos_email,
|
||||
)
|
||||
destinatario = email.get("sender")
|
||||
if destinatario and destinatario != "Desconocido":
|
||||
outlook_utilities.send_email(
|
||||
token, email_addr,
|
||||
[destinatario],
|
||||
f"Draft SAP B1 - {serie}-{numero}",
|
||||
html_body, adjuntos_email,
|
||||
cc_recipients=["jlopez@aberio.com.pe", "gcalle@pucp.pe"],
|
||||
)
|
||||
else:
|
||||
_log("⚠️ No se pudo determinar el remitente, notificación no enviada")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in new issue
Block a user