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
d023f14ad9
commit
c532170ac2
230 files changed
+21397
-475
No files matched your search
+96
-19
@@ -14,7 +14,8 @@ import hana_manager
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
load_dotenv(dotenv_path=".env")
|
||||
# Ruta absoluta: no depende del directorio de trabajo desde el que se lance el proceso
|
||||
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env"))
|
||||
|
||||
SAPB1_URL = os.getenv("SAP_B1_URL", "")
|
||||
SAPB1_USER = os.getenv("SAP_B1_USER", "")
|
||||
@@ -101,7 +102,7 @@ def _log_response(method: str, endpoint: str, response: requests.Response, paylo
|
||||
f" ({len(response.content)} bytes)"
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
print("⚠️ %s - %s", log_line, response.text[:300])
|
||||
print(f"⚠️ {log_line} - {response.text[:300]}")
|
||||
else:
|
||||
print(log_line)
|
||||
|
||||
@@ -204,24 +205,45 @@ def get_purchase_invoice(doc_entry: int) -> dict:
|
||||
return result
|
||||
|
||||
# ============================================================================
|
||||
# OBTENER GUIA (GET /DeliveryNotes)
|
||||
# Tabla HANA de origen → entidad de Service Layer que hay que consultar.
|
||||
# ODLN (Entregas) se expone en SL como DeliveryNotes; OWTR (Traslados de
|
||||
# Inventario) se expone como StockTransfers.
|
||||
# ============================================================================
|
||||
_TABLA_A_ENTIDAD_SL = {
|
||||
"ODLN": "DeliveryNotes",
|
||||
"OWTR": "StockTransfers",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# OBTENER GUIA (GET /DeliveryNotes o /StockTransfers)
|
||||
# ============================================================================
|
||||
def get_delivery_notes(tipo: str, serie: str, numero: str) -> list:
|
||||
"""
|
||||
Obtiene DeliveryNotes de SAP B1.
|
||||
Obtiene el documento completo de SAP B1 a partir del cual se validan los
|
||||
precios de flete (líneas ``FL*`` en ``DocumentLines``).
|
||||
|
||||
Fase 1 — Busca DocEntry/DocNum/CardCode/CardName:
|
||||
- Primero intenta Service Layer con $select.
|
||||
- Si no hay resultados, fallback a HANA (consulta ODLN con LIKE).
|
||||
Fase 1 — Busca DocEntry/DocNum/CardCode/CardName/Entidad:
|
||||
- Primero intenta Service Layer con $select (entidad DeliveryNotes).
|
||||
- Si no hay resultados, fallback a HANA (consulta ODLN y, si no
|
||||
encuentra nada, OWTR).
|
||||
|
||||
Fase 2 — Por cada DocEntry obtenido, hace GET /DeliveryNotes({DocEntry})
|
||||
para recuperar el documento completo.
|
||||
Fase 2 — Por cada DocEntry obtenido, hace GET a la entidad indicada en
|
||||
"Entidad" (DeliveryNotes o StockTransfers).
|
||||
- Si es DeliveryNotes, ese documento ya trae ``DocumentLines`` con
|
||||
las líneas FL* y se retorna tal cual.
|
||||
- Si es StockTransfers, ese documento es un traslado de inventario
|
||||
y NO tiene líneas FL* ni precios. En ese caso se lee el campo
|
||||
``U_IDC_NUMOV`` (DocEntry de la Orden de Venta asociada) y se
|
||||
hace GET a ``Orders(U_IDC_NUMOV)``, devolviendo ese documento en
|
||||
su lugar para que la validación de precios funcione igual que
|
||||
con DeliveryNotes.
|
||||
|
||||
Returns:
|
||||
list[dict] — Documentos completos de DeliveryNotes, o [] si no se
|
||||
encontró nada en ninguna fuente.
|
||||
list[dict] — Documentos completos, o [] si no se encontró nada en
|
||||
ninguna fuente.
|
||||
"""
|
||||
codigo = f"{serie}{tipo}-{numero}"
|
||||
codigo = f"{tipo}{serie}-{numero}"
|
||||
|
||||
# ── Fase 1: Obtener DocEntries ──────────────────────────────────────
|
||||
entries = _get_delivery_entries(tipo, serie, numero)
|
||||
@@ -234,27 +256,38 @@ def get_delivery_notes(tipo: str, serie: str, numero: str) -> list:
|
||||
docs = []
|
||||
for entry in entries:
|
||||
doc_entry = entry["DocEntry"]
|
||||
entidad = entry["Entidad"]
|
||||
try:
|
||||
resp = forward_to_sapb1("GET", f"DeliveryNotes({doc_entry})")
|
||||
resp = forward_to_sapb1("GET", f"{entidad}({doc_entry})")
|
||||
doc = resp.json()
|
||||
print(f" │ └─ {entidad}({doc_entry}) obtenido")
|
||||
|
||||
if entidad == "StockTransfers":
|
||||
num_ov = doc.get("U_IDC_NUMOV")
|
||||
if not num_ov:
|
||||
print(f" │ ⚠️ StockTransfers({doc_entry}) sin U_IDC_NUMOV: no se puede validar precios")
|
||||
continue
|
||||
resp_ov = forward_to_sapb1("GET", f"Orders({num_ov})")
|
||||
doc = resp_ov.json()
|
||||
print(f" │ └─ Orders({num_ov}) obtenido (Orden de Venta de StockTransfers({doc_entry}))")
|
||||
|
||||
docs.append(doc)
|
||||
print(f" │ └─ DeliveryNote({doc_entry}) obtenido")
|
||||
except Exception as e:
|
||||
print(f" │ ⚠️ Error obteniendo DeliveryNote({doc_entry}): {e}")
|
||||
print(f" │ ⚠️ Error obteniendo {entidad}({doc_entry}): {e}")
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
def _get_delivery_entries(tipo: str, serie: str, numero: str) -> list:
|
||||
"""Fase 1: intenta SL, fallback a HANA. Retorna [{DocEntry, DocNum, CardCode, CardName}, ...]."""
|
||||
codigo = f"{serie}{tipo}-{numero}"
|
||||
"""Fase 1: intenta SL, fallback a HANA. Retorna [{DocEntry, DocNum, CardCode, CardName, Entidad}, ...]."""
|
||||
codigo = f"{tipo}{serie}-{numero}"
|
||||
|
||||
# ── Intentar Service Layer ───────────────────────────────────────────
|
||||
try:
|
||||
filter_str = (
|
||||
f"Cancelled eq 'tNO'"
|
||||
f" and U_BPP_MDSD eq '{tipo}'"
|
||||
f" and U_BPP_MDTD eq '{serie}'"
|
||||
f" and U_BPP_MDTD eq '{tipo}'"
|
||||
f" and U_BPP_MDSD eq '{serie}'"
|
||||
f" and contains(U_BPP_MDCD,'{numero}')"
|
||||
)
|
||||
resp = forward_to_sapb1(
|
||||
@@ -267,6 +300,8 @@ def _get_delivery_entries(tipo: str, serie: str, numero: str) -> list:
|
||||
)
|
||||
sl_entries = resp.json().get("value", [])
|
||||
if sl_entries:
|
||||
for sl_entry in sl_entries:
|
||||
sl_entry["Entidad"] = "DeliveryNotes"
|
||||
print(f" │ 🔍 Guía {codigo}: SL → {len(sl_entries)} DocEntry encontrados")
|
||||
return sl_entries
|
||||
|
||||
@@ -279,6 +314,10 @@ def _get_delivery_entries(tipo: str, serie: str, numero: str) -> list:
|
||||
try:
|
||||
hana_entries = hana_manager.get_delivery(tipo, serie, numero)
|
||||
if hana_entries:
|
||||
for hana_entry in hana_entries:
|
||||
hana_entry["Entidad"] = _TABLA_A_ENTIDAD_SL.get(
|
||||
hana_entry.get("Tabla"), "DeliveryNotes"
|
||||
)
|
||||
print(f" │ 🔍 Guía {codigo}: HANA → {len(hana_entries)} DocEntry encontrados")
|
||||
return hana_entries
|
||||
print(f" │ 🔍 Guía {codigo}: HANA → 0 registros")
|
||||
@@ -287,6 +326,44 @@ def _get_delivery_entries(tipo: str, serie: str, numero: str) -> list:
|
||||
|
||||
return []
|
||||
|
||||
# ============================================================================
|
||||
# OBTENER CONDICIÓN DE PAGO (GET /BusinessPartners + /PaymentTermsTypes)
|
||||
# ============================================================================
|
||||
def obtener_condicion_pago(card_code: str) -> dict | None:
|
||||
"""
|
||||
Obtiene la condición de pago (PaymentTermsTypes) de un Business Partner,
|
||||
para calcular la fecha de vencimiento de un documento.
|
||||
|
||||
Args:
|
||||
card_code: Código del Business Partner (ej. "PL20555929006").
|
||||
|
||||
Returns:
|
||||
dict | None — {"GroupNumber", "PaymentTermsGroupName",
|
||||
"NumberOfAdditionalMonths", "NumberOfAdditionalDays"}, o None si el
|
||||
CardCode no existe, no tiene condición de pago asociada, o falló la
|
||||
consulta a Service Layer.
|
||||
"""
|
||||
try:
|
||||
resp_bp = forward_to_sapb1(
|
||||
"GET", f"BusinessPartners('{card_code}')",
|
||||
params={"$select": "CardCode,PayTermsGrpCode"},
|
||||
)
|
||||
group_num = resp_bp.json().get("PayTermsGrpCode")
|
||||
if group_num is None:
|
||||
return None
|
||||
|
||||
resp_terms = forward_to_sapb1("GET", f"PaymentTermsTypes({group_num})")
|
||||
terms = resp_terms.json()
|
||||
return {
|
||||
"GroupNumber": terms.get("GroupNumber"),
|
||||
"PaymentTermsGroupName": terms.get("PaymentTermsGroupName"),
|
||||
"NumberOfAdditionalMonths": terms.get("NumberOfAdditionalMonths") or 0,
|
||||
"NumberOfAdditionalDays": terms.get("NumberOfAdditionalDays") or 0,
|
||||
}
|
||||
except Exception as e:
|
||||
print(f" │ ⚠️ Error obteniendo condición de pago de {card_code}: {e}")
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# OBTENER ZONAS (GET /U_IDC_ZONAS)
|
||||
# ============================================================================
|
||||
|
||||
Reference in new issue
Block a user