Files

86 lines
3.7 KiB
Python
Raw Permalink Normal View History

import re
from collections import Counter
from typing import Any, Dict, Iterable, List, Optional
from core.errors import SAPServiceLayerError
from core.sap import SAPServiceLayerClient
from domain.items import identificar_skus_faltantes
def diagnosticar_error(sap: SAPServiceLayerClient, exc: SAPServiceLayerError, item_codes: Optional[List[str]] = None) -> str:
"""
Si el error de SAP corresponde a un tipo conocido, devuelve un mensaje
en lenguaje que el usuario entienda; si no, devuelve el mensaje
original de SAP tal cual (para no ocultar errores no identificados).
"""
if "exchange rate" in (exc.detail or "").lower():
moneda = re.search(r"'(\w+)'", exc.detail or "")
moneda = moneda.group(1) if moneda else "?"
return f"No hay tipo de cambio cargado en SAP para la moneda {moneda}"
if item_codes:
faltantes = identificar_skus_faltantes(sap, item_codes)
if faltantes:
return f"SKU inexistente(s) en SAP: {', '.join(faltantes)} (mensaje original de SAP: {exc.detail})"
return exc.detail
def buscar_documento_existente(
sap: SAPServiceLayerClient,
entidad: str,
store_code: str,
shopify_order_id: Any,
campo_lineas: str,
lineas_esperadas: Optional[List[Dict[str, Any]]],
excluir: Iterable[int] = (),
solo_vigentes: bool = False,
) -> Optional[Dict[str, Any]]:
"""
Resguardo de idempotencia contra SAP: busca un documento ya creado para
esta orden (UDFs U_ShopifyOrderId + U_ShopifyStore) cuyas líneas
(ItemCode, Quantity) coincidan con las que se iban a crear. Sirve cuando
la BD local perdió el registro de un documento que sí existe en SAP
(evita duplicarlo). Devuelve el documento completo o None.
`excluir`: DocEntry que ya están asignados a otro paso de la misma orden.
`solo_vigentes`: descarta anulados y documentos de anulación (Invoices).
Con `lineas_esperadas=None` se acepta el primer documento encontrado sin
comparar líneas (para la Invoice, que es única por orden).
"""
filtro = f"U_ShopifyOrderId eq '{shopify_order_id}' and U_ShopifyStore eq '{store_code}'"
if solo_vigentes:
filtro += " and CancelStatus eq 'csNo'"
candidatos = sap.forward("GET", entidad, params={"$filter": filtro, "$select": "DocEntry"}).json().get("value", [])
esperadas = Counter((l["ItemCode"], float(l["Quantity"])) for l in lineas_esperadas) if lineas_esperadas is not None else None
excluidos = set(excluir)
for candidato in sorted(candidatos, key=lambda c: c["DocEntry"]):
if candidato["DocEntry"] in excluidos:
continue
doc = sap.forward("GET", f"{entidad}({candidato['DocEntry']})").json()
if esperadas is None or Counter((l["ItemCode"], float(l["Quantity"])) for l in doc.get(campo_lineas, [])) == esperadas:
return doc
return None
def crear_documento_sap(
sap: SAPServiceLayerClient,
entidad: str,
payload: Dict[str, Any],
item_codes: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
POST genérico a cualquier entidad de documentos de SAP (Invoices,
IncomingPayments, InventoryTransferRequests, StockTransfers,
DeliveryNotes). Si falla, intenta identificar la causa real (ver
diagnosticar_error) antes de relanzar el error con ese mensaje; si no
se reconoce el error, se relanza el mensaje original de SAP.
"""
try:
response = sap.forward("POST", entidad, json_data=payload)
return response.json()
except SAPServiceLayerError as exc:
mensaje = diagnosticar_error(sap, exc, item_codes)
if mensaje != exc.detail:
raise SAPServiceLayerError(exc.status_code, mensaje, code=exc.code) from exc
raise