proyecto en producción
This commit is contained in:
commit
457ca296fa
9 files changed
+1009
No files matched your search
+12
@@ -0,0 +1,12 @@
|
|||||||
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
logs/
|
||||||
|
.env.dev
|
||||||
|
.DS
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
ESTADOS_COMPROBANTE = {
|
||||||
|
"0": "NO EXISTE",
|
||||||
|
"1": "ACEPTADO",
|
||||||
|
"2": "ANULADO",
|
||||||
|
"3": "AUTORIZADO",
|
||||||
|
"4": "NO AUTORIZADO"
|
||||||
|
}
|
||||||
|
|
||||||
|
ESTADOS_CONTRIBUYENTE = {
|
||||||
|
"00": "ACTIVO",
|
||||||
|
"01": "BAJA PROVISIONAL",
|
||||||
|
"02": "BAJA PROV. POR OFICIO",
|
||||||
|
"03": "SUSPENSION TEMPORAL",
|
||||||
|
"10": "BAJA DEFINITIVA",
|
||||||
|
"11": "BAJA OFICIO",
|
||||||
|
"22": "INHABILITADO-VENT.UNICA"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONDICIONES_DOMICILIADO = {
|
||||||
|
"00": "HABIDO",
|
||||||
|
"09": "PENDIENTE",
|
||||||
|
"11": "POR VERIFICAR",
|
||||||
|
"12": "NO HABIDO",
|
||||||
|
"20": "NO HALLADO"
|
||||||
|
}
|
||||||
|
|
||||||
|
TABLAS = {
|
||||||
|
"18": "OPCH",
|
||||||
|
"19": "ORPC"
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"CompanyDB": "SBO_TAGUMEDICA_21",
|
||||||
|
"Client_ID": "f0e69d77-c1a4-43a9-8b12-d1d03009b374",
|
||||||
|
"Client_Secret": "LgfpmMh6SehsUOmcfsXIuw==",
|
||||||
|
"Ruc": "20503662869",
|
||||||
|
"Locked": "N",
|
||||||
|
"UserName": "manager",
|
||||||
|
"Password": "m1r1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompanyDB": "SBO_SAFENA",
|
||||||
|
"Client_ID": "f4ecb759-a991-4aee-abe5-eb474993dcbb",
|
||||||
|
"Client_Secret": "RNBaHKShBC3lFdquGwe/gg==",
|
||||||
|
"Ruc": "20603837721",
|
||||||
|
"Locked": "N",
|
||||||
|
"UserName": "manager",
|
||||||
|
"Password": "m1r1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompanyDB": "SBO_CIRUGIA",
|
||||||
|
"Client_ID": "21d41835-0289-4b3c-99c0-ff0591d2233d",
|
||||||
|
"Client_Secret": "hzKoLbgjRqLUmNhsLagb7w==",
|
||||||
|
"Ruc": "20521486857",
|
||||||
|
"Locked": "N",
|
||||||
|
"UserName": "manager",
|
||||||
|
"Password": "m1r1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompanyDB": "SBO_FRANQUICIA",
|
||||||
|
"Client_ID": "516d9f51-f7c3-4d29-ba04-4d97b406773f",
|
||||||
|
"Client_Secret": "LZ1JO7osqAccSm3/8sGMSQ==",
|
||||||
|
"Ruc": "20600278259",
|
||||||
|
"Locked": "N",
|
||||||
|
"UserName": "manager",
|
||||||
|
"Password": "m1r1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompanyDB": "SBO_CALICAY",
|
||||||
|
"Client_ID": "d7ceb759-b6dc-4d97-a0c7-42bfc5226c53",
|
||||||
|
"Client_Secret": "MIjVHUtvqEIeE06M35nOuQ==",
|
||||||
|
"Ruc": "20611487143",
|
||||||
|
"Locked": "N",
|
||||||
|
"UserName": "manager",
|
||||||
|
"Password": "m1r1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CompanyDB": "SBO_ELAVENT",
|
||||||
|
"Client_ID": "9b67b816-d900-48e6-85c3-c9ba78b58040",
|
||||||
|
"Client_Secret": "xgnnvxeU1ZYTABPzhUAxxA==",
|
||||||
|
"Ruc": "20613152700",
|
||||||
|
"Locked": "N",
|
||||||
|
"UserName": "manager",
|
||||||
|
"Password": "m1r1"
|
||||||
|
}
|
||||||
|
]
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
from hdbcli import dbapi
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from constants import TABLAS, ESTADOS_COMPROBANTE, ESTADOS_CONTRIBUYENTE, CONDICIONES_DOMICILIADO
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURACIÓN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Cargar variables de entorno desde .env
|
||||||
|
load_dotenv(dotenv_path=".env.dev")
|
||||||
|
|
||||||
|
# Configuración de la API
|
||||||
|
HANA_HOST = os.getenv("HANA_HOST", "")
|
||||||
|
HANA_PORT = int(os.getenv("HANA_PORT", 30015))
|
||||||
|
HANA_USER = os.getenv("HANA_USER", "")
|
||||||
|
HANA_PASSWORD = os.getenv("HANA_PASSWORD", "")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def get_connection():
|
||||||
|
"""Crea y retorna una conexión a SAP HANA."""
|
||||||
|
try:
|
||||||
|
conn = dbapi.connect(
|
||||||
|
address=HANA_HOST,
|
||||||
|
port=HANA_PORT,
|
||||||
|
user=HANA_USER,
|
||||||
|
password=HANA_PASSWORD
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error conectando a HANA: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def obtener_documentos_creados_actualizados_hace_x_minutos(schema, minutes=60):
|
||||||
|
"""
|
||||||
|
Se conecta a la base de datos SAP HANA, realiza una consulta a la tabla OPCH,
|
||||||
|
y formatea el resultado para que coincida con el esquema de documentos.json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT T0."ObjType", T0."DocEntry", T0."DocNum", T0."CardCode", T0."CardName",
|
||||||
|
TO_VARCHAR(T0."DocDate",'DD/MM/YYYY') "DocDate",
|
||||||
|
TO_VARCHAR(T0."TaxDate",'DD/MM/YYYY') "TaxDate",
|
||||||
|
TO_VARCHAR(T0."CreateDate",'DD/MM/YYYY') "CreateDate",
|
||||||
|
T3."LicTradNum" "LicTradNum",
|
||||||
|
T1."USER_CODE" "UserCode",
|
||||||
|
T0."Indicator", T0."FolioPref", T0."FolioNum",
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(T0."DocCur",'SOL') = 'SOL'
|
||||||
|
THEN T0."DocTotal" + IFNULL((SELECT SUM(T5."WTAmnt") FROM "{schema}".PCH5 T5 WHERE T5."AbsEntry" = T0."DocEntry" AND T5."Category" = 'I'),0.00)
|
||||||
|
ELSE T0."DocTotalFC" + IFNULL((SELECT SUM(T5."WTAmntFC") FROM "{schema}".PCH5 T5 WHERE T5."AbsEntry" = T0."DocEntry" AND T5."Category" = 'I'),0.00)
|
||||||
|
END "DocTotal"
|
||||||
|
FROM "{schema}".OPCH T0
|
||||||
|
INNER JOIN "{schema}".OCRD T3 ON T3."CardCode" = T0."CardCode"
|
||||||
|
INNER JOIN "{schema}".OUSR T1 ON T1."USERID" = T0."UserSign2"
|
||||||
|
WHERE TO_TIMESTAMP(TO_VARCHAR(T0."UpdateDate", 'YYYY-MM-DD')
|
||||||
|
|| ' ' || LPAD(TO_VARCHAR(T0."UpdateTS"), 6, '0'),
|
||||||
|
'YYYY-MM-DD HH24MISS')
|
||||||
|
>= ADD_SECONDS(CURRENT_TIMESTAMP, {-1 * minutes * 60})
|
||||||
|
|
||||||
|
AND T0."CANCELED" = 'N'
|
||||||
|
AND IFNULL(T0."Indicator",'') IN ('01','02','03','08')
|
||||||
|
AND LENGTH(IFNULL(T0."FolioPref",'')) > 0
|
||||||
|
AND IFNULL(T0."FolioNum",0) > 0
|
||||||
|
AND IFNULL(T3."U_EXX_TIPODOCU",'') IN ('6','4')
|
||||||
|
AND (LENGTH(IFNULL(T0."U_PGX_STMENSAJE",'')) = 0 OR LOCATE('NO EXISTE', IFNULL(T0."U_PGX_STMENSAJE",'')) > 0)
|
||||||
|
ORDER BY T0."CreateDate" DESC;
|
||||||
|
"""
|
||||||
|
print(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||||
|
connection = get_connection()
|
||||||
|
if not connection:
|
||||||
|
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
|
# Obtener nombres de columnas
|
||||||
|
columns = [column[0] for column in cursor.description]
|
||||||
|
|
||||||
|
# Convertir resultados a lista de diccionarios
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
results.append(dict(zip(columns, row)))
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error en consulta HANA: {e}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def obtener_documentos_fecha_emision_ultimos_8_dias(schema):
|
||||||
|
query = f"""
|
||||||
|
SELECT T0."ObjType", T0."DocEntry", T0."DocNum", T0."CardCode", T0."CardName",
|
||||||
|
TO_VARCHAR(T0."DocDate",'DD/MM/YYYY') "DocDate",
|
||||||
|
TO_VARCHAR(T0."TaxDate",'DD/MM/YYYY') "TaxDate",
|
||||||
|
TO_VARCHAR(T0."CreateDate",'DD/MM/YYYY') "CreateDate",
|
||||||
|
T3."LicTradNum" "LicTradNum",
|
||||||
|
T1."USER_CODE" "UserCode",
|
||||||
|
T0."Indicator", T0."FolioPref", T0."FolioNum",
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(T0."DocCur",'SOL') = 'SOL'
|
||||||
|
THEN T0."DocTotal" + IFNULL((SELECT SUM(T5."WTAmnt") FROM "{schema}".PCH5 T5 WHERE T5."AbsEntry" = T0."DocEntry" AND T5."Category" = 'I'),0.00)
|
||||||
|
ELSE T0."DocTotalFC" + IFNULL((SELECT SUM(T5."WTAmntFC") FROM "{schema}".PCH5 T5 WHERE T5."AbsEntry" = T0."DocEntry" AND T5."Category" = 'I'),0.00)
|
||||||
|
END "DocTotal"
|
||||||
|
FROM "{schema}".OPCH T0
|
||||||
|
INNER JOIN "{schema}".OCRD T3 ON T3."CardCode" = T0."CardCode"
|
||||||
|
INNER JOIN "{schema}".OUSR T1 ON T1."USERID" = T0."UserSign2"
|
||||||
|
WHERE T0."TaxDate" >= ADD_DAYS(CURRENT_DATE, -8)
|
||||||
|
AND T0."CANCELED" = 'N'
|
||||||
|
AND IFNULL(T0."Indicator",'') IN ('01','02','03','08')
|
||||||
|
AND LENGTH(IFNULL(T0."FolioPref",'')) > 0
|
||||||
|
AND IFNULL(T0."FolioNum",0) > 0
|
||||||
|
AND IFNULL(T3."U_EXX_TIPODOCU",'') IN ('6','4')
|
||||||
|
ORDER BY T0."U_PGX_STMENSAJE", T0."CreateDate" DESC;
|
||||||
|
"""
|
||||||
|
print(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||||
|
connection = get_connection()
|
||||||
|
if not connection:
|
||||||
|
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
|
columns = [column[0] for column in cursor.description]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
results.append(dict(zip(columns, row)))
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error en consulta HANA: {e}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def obtener_documentos_creados_hace_x_dias(schema, dias=8):
|
||||||
|
query = f"""
|
||||||
|
SELECT T0."ObjType", T0."DocEntry", T0."DocNum", T0."CardCode", T0."CardName",
|
||||||
|
TO_VARCHAR(T0."DocDate",'DD/MM/YYYY') "DocDate",
|
||||||
|
TO_VARCHAR(T0."TaxDate",'DD/MM/YYYY') "TaxDate",
|
||||||
|
TO_VARCHAR(T0."CreateDate",'DD/MM/YYYY') "CreateDate",
|
||||||
|
T3."LicTradNum" "LicTradNum",
|
||||||
|
T1."USER_CODE" "UserCode",
|
||||||
|
T0."Indicator", T0."FolioPref", T0."FolioNum",
|
||||||
|
CASE
|
||||||
|
WHEN IFNULL(T0."DocCur",'SOL') = 'SOL'
|
||||||
|
THEN T0."DocTotal" + IFNULL((SELECT SUM(T5."WTAmnt") FROM "{schema}".PCH5 T5 WHERE T5."AbsEntry" = T0."DocEntry" AND T5."Category" = 'I'),0.00)
|
||||||
|
ELSE T0."DocTotalFC" + IFNULL((SELECT SUM(T5."WTAmntFC") FROM "{schema}".PCH5 T5 WHERE T5."AbsEntry" = T0."DocEntry" AND T5."Category" = 'I'),0.00)
|
||||||
|
END "DocTotal"
|
||||||
|
FROM "{schema}".OPCH T0
|
||||||
|
INNER JOIN "{schema}".OCRD T3 ON T3."CardCode" = T0."CardCode"
|
||||||
|
INNER JOIN "{schema}".OUSR T1 ON T1."USERID" = T0."UserSign2"
|
||||||
|
WHERE T0."CreateDate" >= ADD_DAYS(CURRENT_DATE, {-1 * dias})
|
||||||
|
AND T0."CANCELED" = 'N'
|
||||||
|
AND (LENGTH(IFNULL(T0."U_PGX_STMENSAJE",'')) = 0 OR LOCATE('NO EXISTE', IFNULL(T0."U_PGX_STMENSAJE",'')) > 0)
|
||||||
|
AND IFNULL(T0."Indicator",'') IN ('01','02','03','08')
|
||||||
|
AND LENGTH(IFNULL(T0."FolioPref",'')) > 0
|
||||||
|
AND IFNULL(T0."FolioNum",0) > 0
|
||||||
|
AND IFNULL(T3."U_EXX_TIPODOCU",'') IN ('6','4')
|
||||||
|
ORDER BY T0."CreateDate" DESC;
|
||||||
|
"""
|
||||||
|
print(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||||
|
connection = get_connection()
|
||||||
|
if not connection:
|
||||||
|
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
|
columns = [column[0] for column in cursor.description]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
results.append(dict(zip(columns, row)))
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error en consulta HANA: {e}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def actualizar_documento_sap(schema, registro,resultado):
|
||||||
|
|
||||||
|
estado_sunat = "4"
|
||||||
|
estado_comprobante = ""
|
||||||
|
|
||||||
|
if resultado["success"] == True:
|
||||||
|
error = "200"
|
||||||
|
observaciones = ""
|
||||||
|
d = resultado.get("data") or {}
|
||||||
|
|
||||||
|
if not any(d.values()):
|
||||||
|
mensaje = "SIN_DATOS|||"
|
||||||
|
estado_comprobante = "0"
|
||||||
|
estado_sunat = "2"
|
||||||
|
else:
|
||||||
|
observaciones = d.get("observaciones", [""])[0] if d.get("observaciones") else ""
|
||||||
|
estado_comprobante = d.get("estadoCp", "")
|
||||||
|
estado_contribuyente = d.get("estadoRuc", "")
|
||||||
|
condicion_domicilio = d.get("condDomiRuc", "")
|
||||||
|
|
||||||
|
mensaje = "" if estado_comprobante == "" else ESTADOS_COMPROBANTE[estado_comprobante]
|
||||||
|
mensaje += "|" if estado_contribuyente == "" else "|" + ESTADOS_CONTRIBUYENTE[estado_contribuyente]
|
||||||
|
mensaje += "|" if condicion_domicilio == "" else "|" + CONDICIONES_DOMICILIADO[condicion_domicilio]
|
||||||
|
mensaje += "|" + observaciones
|
||||||
|
|
||||||
|
estado_sunat = "2" if estado_comprobante == "" else ("0" if estado_comprobante == "1" else "2")
|
||||||
|
|
||||||
|
if resultado["success"] == False:
|
||||||
|
error = "400"
|
||||||
|
mensaje = resultado["message"]
|
||||||
|
|
||||||
|
mensaje_limpio = re.sub(r'[^0-9a-zA-Z .|#-]',' ',mensaje)
|
||||||
|
|
||||||
|
connection = get_connection()
|
||||||
|
if not connection:
|
||||||
|
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
|
||||||
|
# actualizando la tabla SAP
|
||||||
|
tabla = TABLAS[registro["objtype"]]
|
||||||
|
|
||||||
|
query = f"""UPDATE "{registro["companydb"]}".{tabla}
|
||||||
|
SET "U_PGX_STMENSAJE" = '{mensaje_limpio}',
|
||||||
|
"U_PGX_SSTADO" = '{estado_sunat}',
|
||||||
|
"U_PGX_PETOTORFECHA" = CURRENT_DATE,
|
||||||
|
"U_PGX_PETOTORHORA" = TO_VARCHAR(CURRENT_TIME,'HH24:MM:SS')
|
||||||
|
WHERE "DocEntry" = {registro["fieldvalue"]};"""
|
||||||
|
|
||||||
|
print(f" ┌─ 🗄️ UPDATE HANA ──────────────────────")
|
||||||
|
for line in query.splitlines():
|
||||||
|
print(f" │ {line}")
|
||||||
|
print(f" └─────────────────────────────────────────")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(query)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error actualizando registro en HANA: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
_stdout_original = sys.stdout
|
||||||
|
_stderr_original = sys.stderr
|
||||||
|
_file_stream = None
|
||||||
|
_log_dir = "logs"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
class MultiStream:
|
||||||
|
def __init__(self, *streams):
|
||||||
|
self.streams = list(streams)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
for stream in self.streams:
|
||||||
|
stream.write(data)
|
||||||
|
stream.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
for stream in self.streams:
|
||||||
|
stream.flush()
|
||||||
|
|
||||||
|
def reemplazar_file(self, old_fh, new_fh):
|
||||||
|
for i, s in enumerate(self.streams):
|
||||||
|
if s is old_fh:
|
||||||
|
self.streams[i] = new_fh
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def _ruta_log(hoy=None):
|
||||||
|
if hoy is None:
|
||||||
|
hoy = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
if not os.path.exists(_log_dir):
|
||||||
|
os.makedirs(_log_dir)
|
||||||
|
return os.path.join(_log_dir, f"{hoy}.log")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def setup_logger():
|
||||||
|
global _file_stream
|
||||||
|
log_path = _ruta_log()
|
||||||
|
_file_stream = open(log_path, "a", encoding="utf-8")
|
||||||
|
sys.stdout = MultiStream(_stdout_original, _file_stream)
|
||||||
|
sys.stderr = MultiStream(_stderr_original, _file_stream)
|
||||||
|
return log_path
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def rotar_log():
|
||||||
|
global _file_stream
|
||||||
|
hoy = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
log_path = _ruta_log(hoy)
|
||||||
|
|
||||||
|
if _file_stream and os.path.abspath(_file_stream.name) == os.path.abspath(log_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
nuevo_fh = open(log_path, "a", encoding="utf-8")
|
||||||
|
if isinstance(sys.stdout, MultiStream):
|
||||||
|
sys.stdout.reemplazar_file(_file_stream, nuevo_fh)
|
||||||
|
if isinstance(sys.stderr, MultiStream):
|
||||||
|
sys.stderr.reemplazar_file(_file_stream, nuevo_fh)
|
||||||
|
try:
|
||||||
|
_file_stream.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_file_stream = nuevo_fh
|
||||||
|
print(f"📄 Log rotado a: {log_path}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def limpiar_logs_antiguos(dias=7):
|
||||||
|
"""
|
||||||
|
Elimina archivos de log con más de 'n' días de antigüedad.
|
||||||
|
"""
|
||||||
|
log_dir = "logs"
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
return
|
||||||
|
|
||||||
|
limite = datetime.now() - timedelta(days=dias)
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for archivo in os.listdir(log_dir):
|
||||||
|
ruta_archivo = os.path.join(log_dir, archivo)
|
||||||
|
if os.path.isfile(ruta_archivo) and archivo.endswith(".log"):
|
||||||
|
fecha_archivo = datetime.fromtimestamp(os.path.getmtime(ruta_archivo))
|
||||||
|
if fecha_archivo < limite:
|
||||||
|
try:
|
||||||
|
os.remove(ruta_archivo)
|
||||||
|
count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error al eliminar log {archivo}: {e}")
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
print(f"🧹 Se eliminaron {count} archivos de log antiguos (más de {dias} días).")
|
||||||
|
# ============================================================================
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
import logger_utilities
|
||||||
|
import sunat_manager
|
||||||
|
import hana_manager
|
||||||
|
import sapb1_manager
|
||||||
|
from constants import ESTADOS_COMPROBANTE
|
||||||
|
from hana_manager import actualizar_documento_sap
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def es_hora_pico(dt: datetime) -> bool:
|
||||||
|
minutos = dt.hour * 60 + dt.minute
|
||||||
|
return (360 <= minutos < 795) or (840 <= minutos < 1050)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def cargar_empresas():
|
||||||
|
with open("empresas.json", "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def enviar_mensaje_sl(usercode, doc, resultado, schema):
|
||||||
|
estado_cp = resultado.get("data", {}).get("estadoCp", "")
|
||||||
|
estado_texto = ESTADOS_COMPROBANTE.get(estado_cp, "DESCONOCIDO")
|
||||||
|
|
||||||
|
subject = f"❌ SUNAT - {doc['Indicator']}-{doc['FolioPref']}-{doc['FolioNum']}"
|
||||||
|
observaciones = resultado.get("data", {}).get("observaciones", [""])[0] or ""
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"El documento {doc['Indicator']}-{doc['FolioPref']}-{doc['FolioNum']} "
|
||||||
|
f"de {schema} entrego el siguiente estado por SUNAT.\n"
|
||||||
|
f"Estado: {estado_texto} (código: {estado_cp})\n"
|
||||||
|
f"RUC Proveedor: {doc.get('LicTradNum', 'N/A')}\n"
|
||||||
|
f"Razón Social: {doc.get('CardName', 'N/A')}\n"
|
||||||
|
f"Mensaje SUNAT: {resultado.get('message', 'N/A')}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if observaciones:
|
||||||
|
text += f"Observaciones SUNAT: {observaciones}\n"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"MessageDataColumns": [
|
||||||
|
{
|
||||||
|
"ColumnName": "Numero",
|
||||||
|
"Link": "tNO",
|
||||||
|
"MessageDataLines": [
|
||||||
|
{
|
||||||
|
"Object":"",
|
||||||
|
"ObjectKey":"",
|
||||||
|
"Value":f"{doc['Indicator']}-{doc['FolioPref']}-{doc['FolioNum']} "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ColumnName": "Documento",
|
||||||
|
"Link": "tYES",
|
||||||
|
"MessageDataLines": [
|
||||||
|
{
|
||||||
|
"Object": str(doc.get("ObjType", "18")),
|
||||||
|
"ObjectKey": str(doc["DocEntry"]),
|
||||||
|
"Value": str(doc["DocNum"])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ColumnName": "Razón Social / Nombre",
|
||||||
|
"Link": "tNO",
|
||||||
|
"MessageDataLines": [
|
||||||
|
{
|
||||||
|
"Object": "",
|
||||||
|
"ObjectKey": "",
|
||||||
|
"Value": f"{doc.get('CardName', 'N/A')}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RecipientCollection": [
|
||||||
|
{
|
||||||
|
"SendInternal": "tYES",
|
||||||
|
"UserCode": usercode
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Subject": subject,
|
||||||
|
"Text": text
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print(f" ┌─ 📩 Enviando Messages a {usercode} ───────────")
|
||||||
|
# for line in json.dumps(payload, indent=2).splitlines():
|
||||||
|
# print(f" │ {line}")
|
||||||
|
sapb1_manager.send_message(payload)
|
||||||
|
print(f" │ ✅ Mensaje enviado a {usercode}")
|
||||||
|
print(f" └───────────────────────────────────────────────")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def procesar_empresa(schema, datos_empresa, modo):
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"🏢 Procesando empresa: {schema}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
if modo == "rapido":
|
||||||
|
documentos = hana_manager.obtener_documentos_creados_actualizados_hace_x_minutos(schema, 60)
|
||||||
|
else:
|
||||||
|
documentos = hana_manager.obtener_documentos_fecha_emision_ultimos_8_dias(schema)
|
||||||
|
if not documentos:
|
||||||
|
print(f"⚠️ No se encontraron documentos para {schema}")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
## documentos = hana_manager.obtener_documentos_creados_hace_x_dias(schema, 8)
|
||||||
|
|
||||||
|
print(f"📄 Documentos encontrados: {len(documentos)}")
|
||||||
|
|
||||||
|
print(f"🔑 Solicitando token SUNAT...")
|
||||||
|
token = sunat_manager.token(datos_empresa)
|
||||||
|
if not token:
|
||||||
|
print(f"❌ No se obtuvo token para {schema}")
|
||||||
|
return 0, len(documentos)
|
||||||
|
|
||||||
|
print(f"✅ Token obtenido correctamente")
|
||||||
|
|
||||||
|
sapb1_manager.login()
|
||||||
|
print(f"✅ Sesión SL establecida para {schema}")
|
||||||
|
|
||||||
|
ruc_empresa = datos_empresa["Ruc"]
|
||||||
|
procesados = 0
|
||||||
|
errores = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for idx, doc in enumerate(documentos, start=1):
|
||||||
|
print(f"\n--- [{idx}/{len(documentos)}] {doc['FolioPref']}-{doc['FolioNum']} ---")
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"numRuc": doc["LicTradNum"],
|
||||||
|
"codComp": "R1" if doc["Indicator"] == "02" else doc["Indicator"],
|
||||||
|
"numeroSerie": doc["FolioPref"],
|
||||||
|
"numero": doc["FolioNum"],
|
||||||
|
"fechaEmision": doc["TaxDate"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc["FolioPref"] and doc["FolioPref"][0].isalpha():
|
||||||
|
payload["monto"] = str(doc["DocTotal"])
|
||||||
|
|
||||||
|
print(f" ┌─ 📤 Enviando a SUNAT {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}─────────────────────")
|
||||||
|
for line in json.dumps(payload, indent=2).splitlines():
|
||||||
|
print(f" │ {line}")
|
||||||
|
print(f" └────────────────────────────────────────────")
|
||||||
|
resultado = sunat_manager.validar_comprobante(token, ruc_empresa, payload)
|
||||||
|
print(f" ┌─ 📥 Respuesta de SUNAT {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}───────────────────")
|
||||||
|
for line in json.dumps(resultado, indent=2).splitlines():
|
||||||
|
print(f" │ {line}")
|
||||||
|
print(f" └────────────────────────────────────────────")
|
||||||
|
|
||||||
|
registro = {
|
||||||
|
"objtype": str(doc["ObjType"]),
|
||||||
|
"companydb": schema,
|
||||||
|
"fieldvalue": doc["DocEntry"],
|
||||||
|
}
|
||||||
|
|
||||||
|
actualizar_documento_sap(schema, registro, resultado)
|
||||||
|
|
||||||
|
if resultado.get("success"):
|
||||||
|
estado_cp = resultado.get("data", {}).get("estadoCp", "")
|
||||||
|
print(f"✅ OK - Estado CP: {estado_cp}")
|
||||||
|
|
||||||
|
if estado_cp not in ["1","3"] and doc.get("UserCode"):
|
||||||
|
enviar_mensaje_sl(doc["UserCode"], doc, resultado, schema)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Falló - {resultado.get('message', 'Error desconocido')}")
|
||||||
|
|
||||||
|
procesados += 1
|
||||||
|
|
||||||
|
if idx < len(documentos):
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error procesando documento: {e}")
|
||||||
|
errores += 1
|
||||||
|
finally:
|
||||||
|
sapb1_manager.logout()
|
||||||
|
print(f"🔒 Sesión SL cerrada para {schema}")
|
||||||
|
|
||||||
|
return procesados, errores
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def main():
|
||||||
|
log_path = logger_utilities.setup_logger()
|
||||||
|
logger_utilities.limpiar_logs_antiguos(dias=7)
|
||||||
|
|
||||||
|
load_dotenv(dotenv_path=".env.dev")
|
||||||
|
|
||||||
|
empresas = cargar_empresas()
|
||||||
|
print(f"📋 Empresas cargadas: {len(empresas)}")
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("🚀 Demonio iniciado")
|
||||||
|
print(f"📄 Log guardado en: {log_path}")
|
||||||
|
print(" Horario pico (cada 10 seg): 06:00 - 13:15 | 14:00 - 17:30")
|
||||||
|
print(" Horario valle (cada 30 min): resto del día")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
inicio_ciclo = datetime.now()
|
||||||
|
|
||||||
|
logger_utilities.rotar_log()
|
||||||
|
logger_utilities.limpiar_logs_antiguos(dias=7)
|
||||||
|
|
||||||
|
modo = "rapido" if es_hora_pico(inicio_ciclo) else "lento"
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"🔄 Ciclo [{modo.upper()}] {inicio_ciclo.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
total_procesados = 0
|
||||||
|
total_errores = 0
|
||||||
|
|
||||||
|
for empresa in empresas:
|
||||||
|
schema = empresa["CompanyDB"]
|
||||||
|
|
||||||
|
if empresa.get("Locked") == "Y":
|
||||||
|
print(f"\n⏭️ Empresa {schema} está bloqueada (LOCKED=Y), saltando...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
sapb1_manager._sap_credentials = {
|
||||||
|
"company": empresa["CompanyDB"],
|
||||||
|
"username": empresa["UserName"],
|
||||||
|
"password": empresa["Password"],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
p, e = procesar_empresa(schema, empresa, modo)
|
||||||
|
total_procesados += p
|
||||||
|
total_errores += e
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"❌ Error procesando empresa {schema}: {ex}")
|
||||||
|
total_errores += 1
|
||||||
|
|
||||||
|
duracion_ciclo = datetime.now() - inicio_ciclo
|
||||||
|
print(f"\n📊 CICLO COMPLETADO en {str(duracion_ciclo).split('.')[0]}")
|
||||||
|
print(f" ✅ {total_procesados} procesados | ❌ {total_errores} errores")
|
||||||
|
|
||||||
|
transcurrido = duracion_ciclo.total_seconds()
|
||||||
|
modo_ahora = "rapido" if es_hora_pico(datetime.now()) else "lento"
|
||||||
|
intervalo = 10 if modo_ahora == "rapido" else 1800
|
||||||
|
dormir = max(0, intervalo - transcurrido)
|
||||||
|
|
||||||
|
print(f"💤 Próximo ciclo [{modo_ahora}] en {dormir:.0f}s...")
|
||||||
|
time.sleep(dormir)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n🛑 Demonio detenido por el usuario.")
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
certifi==2026.6.17
|
||||||
|
charset-normalizer==3.4.9
|
||||||
|
dotenv==0.9.9
|
||||||
|
hdbcli==2.29.23
|
||||||
|
idna==3.18
|
||||||
|
python-dotenv==1.2.2
|
||||||
|
requests==2.34.2
|
||||||
|
urllib3==2.7.0
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURACIÓN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
load_dotenv(dotenv_path=".env.dev")
|
||||||
|
|
||||||
|
SAPB1_URL = os.getenv("SAP_B1_URL", "")
|
||||||
|
SAPB1_TIMEOUT = float(os.getenv("SAP_B1_TIMEOUT", 15.0))
|
||||||
|
SAPB1_VERIFY_SSL = os.getenv("SAP_B1_VERIFY_SSL", "False").lower() == "true"
|
||||||
|
|
||||||
|
# Almacenamiento global de sesión
|
||||||
|
_sap_session: dict = {"headers": None, "b1session": None, "routeid": None}
|
||||||
|
_sap_credentials: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LOGIN
|
||||||
|
# ============================================================================
|
||||||
|
def login() -> dict:
|
||||||
|
if not _sap_credentials:
|
||||||
|
raise RuntimeError("No hay credenciales. Asigna sapb1_manager._sap_credentials antes de llamar login().")
|
||||||
|
payload = {
|
||||||
|
"CompanyDB": _sap_credentials["company"],
|
||||||
|
"UserName": _sap_credentials["username"],
|
||||||
|
"Password": _sap_credentials["password"],
|
||||||
|
}
|
||||||
|
print("🔐 Iniciando sesión en SAP B1 Service Layer...")
|
||||||
|
resp = requests.post(
|
||||||
|
f"{SAPB1_URL}/Login",
|
||||||
|
json=payload,
|
||||||
|
timeout=SAPB1_TIMEOUT,
|
||||||
|
verify=SAPB1_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise ConnectionError(
|
||||||
|
f"Login fallido: HTTP {resp.status_code} - {resp.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
b1session = data["SessionId"]
|
||||||
|
routeid = resp.cookies.get("ROUTEID", "")
|
||||||
|
|
||||||
|
headers = {"Cookie": f"B1SESSION={b1session}; ROUTEID={routeid}"}
|
||||||
|
_sap_session.update({"headers": headers, "b1session": b1session, "routeid": routeid})
|
||||||
|
print(f"✅ Sesión SAP B1 establecida (B1SESSION: {b1session[:10]}...)")
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LOGOUT
|
||||||
|
# ============================================================================
|
||||||
|
def logout():
|
||||||
|
if not _sap_session["b1session"]:
|
||||||
|
print("⚠️ No hay sesión activa para cerrar.")
|
||||||
|
return
|
||||||
|
print("🔓 Cerrando sesión SAP B1...")
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{SAPB1_URL}/Logout",
|
||||||
|
headers=_sap_session["headers"],
|
||||||
|
timeout=SAPB1_TIMEOUT,
|
||||||
|
verify=SAPB1_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
print("✅ Sesión SAP B1 cerrada correctamente.")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Logout respondió HTTP {resp.status_code}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"❌ Error al cerrar sesión: {exc}")
|
||||||
|
finally:
|
||||||
|
_sap_session.update({"headers": None, "b1session": None, "routeid": None})
|
||||||
|
_sap_credentials.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SESIÓN (interna - lazy login)
|
||||||
|
# ============================================================================
|
||||||
|
def _ensure_session() -> dict:
|
||||||
|
if _sap_session["headers"]:
|
||||||
|
return _sap_session["headers"]
|
||||||
|
if _sap_credentials:
|
||||||
|
return login()
|
||||||
|
raise RuntimeError("No hay sesión ni credenciales para autenticarse en SAP B1.")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LOGGING DE RESPUESTA
|
||||||
|
# ============================================================================
|
||||||
|
def _log_response(method: str, endpoint: str, response: requests.Response, payload: Any = None):
|
||||||
|
log_line = (
|
||||||
|
f" │ [{method}] {endpoint} → {response.status_code}"
|
||||||
|
f" ({len(response.content)} bytes)"
|
||||||
|
)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
print(f" │ ⚠️ {log_line} - {response.text[:300]}")
|
||||||
|
else:
|
||||||
|
print(log_line)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PROXY CENTRALIZADO
|
||||||
|
# ============================================================================
|
||||||
|
def forward_to_sapb1(
|
||||||
|
method: str,
|
||||||
|
endpoint_path: str,
|
||||||
|
json_data: Optional[Dict[str, Any]] = None,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> requests.Response:
|
||||||
|
if not method or not endpoint_path:
|
||||||
|
raise ValueError(" │ method y endpoint_path son requeridos")
|
||||||
|
|
||||||
|
method = method.upper()
|
||||||
|
headers = _ensure_session()
|
||||||
|
|
||||||
|
url = f"{SAPB1_URL}/{endpoint_path}"
|
||||||
|
req_headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
**_sap_session["headers"],
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f" │ 🔄 [{method}] {endpoint_path} | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
json=json_data,
|
||||||
|
params=params,
|
||||||
|
headers=req_headers,
|
||||||
|
timeout=SAPB1_TIMEOUT,
|
||||||
|
verify=SAPB1_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
raise ConnectionError(f" │ Timeout ({SAPB1_TIMEOUT}s) en {endpoint_path}")
|
||||||
|
except requests.exceptions.ConnectionError as exc:
|
||||||
|
raise ConnectionError(f" │ No se pudo conectar con SAP B1: {exc}")
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
raise ConnectionError(f" │ Error en solicitud HTTP: {exc}")
|
||||||
|
|
||||||
|
if resp.status_code == 401:
|
||||||
|
if not _sap_credentials:
|
||||||
|
raise ConnectionError("Sesión expirada (401) y no hay credenciales para re-autenticar.")
|
||||||
|
print(" │ ⚠️ Sesión expirada (401). Reautenticando e intentando una vez...")
|
||||||
|
login()
|
||||||
|
req_headers["Cookie"] = _sap_session["headers"]["Cookie"]
|
||||||
|
resp = requests.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
json=json_data,
|
||||||
|
params=params,
|
||||||
|
headers=req_headers,
|
||||||
|
timeout=SAPB1_TIMEOUT,
|
||||||
|
verify=SAPB1_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
|
||||||
|
_log_response(method, endpoint_path, resp, json_data)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SEND MESSAGE (POST /Messages)
|
||||||
|
# ============================================================================
|
||||||
|
def send_message(payload: dict) -> dict:
|
||||||
|
resp = forward_to_sapb1("POST", "Messages", json_data=payload)
|
||||||
|
if resp.status_code not in (200, 201):
|
||||||
|
raise ConnectionError(
|
||||||
|
f" │ Error al enviar Mensaje: HTTP {resp.status_code} - {resp.text}"
|
||||||
|
)
|
||||||
|
result = resp.json()
|
||||||
|
print(f" │ ✅ SAP B1 Service Layer - {resp.status_code}")
|
||||||
|
return result
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import urllib3
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Desactivar advertencias de SSL no verificado
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def token(datos):
|
||||||
|
|
||||||
|
CLIENT_ID=datos["Client_ID"]
|
||||||
|
CLIENT_SECRET=datos["Client_Secret"]
|
||||||
|
|
||||||
|
url = f"https://api-seguridad.sunat.gob.pe/v1/clientesextranet/{CLIENT_ID}/oauth2/token/"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Origin': 'https://api-seguridad.sunat.gob.pe'
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = 'grant_type=client_credentials' + '&'
|
||||||
|
payload += 'scope=https://api.sunat.gob.pe/v1/contribuyente/contribuyentes' + '&'
|
||||||
|
payload += f'client_id={CLIENT_ID}' + '&'
|
||||||
|
payload += f'client_secret={CLIENT_SECRET}'
|
||||||
|
|
||||||
|
response = requests.request("POST", url, headers=headers, data=payload, verify=False, timeout=30)
|
||||||
|
|
||||||
|
print(f" 📤 Token request enviado")
|
||||||
|
print(f" 📥 Token response: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if "access_token" in result:
|
||||||
|
token = result["access_token"]
|
||||||
|
return token
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
def validar_comprobante(token,RUC,payload:dict):
|
||||||
|
url = f"https://api.sunat.gob.pe/v1/contribuyente/contribuyentes/{RUC}/validarcomprobante"
|
||||||
|
|
||||||
|
# payload = {
|
||||||
|
# "numRuc": "20538298485",
|
||||||
|
# "codComp": "08",
|
||||||
|
# "numeroSerie": "E001",
|
||||||
|
# "numero": "3773",
|
||||||
|
# "fechaEmision": "15/11/2023",
|
||||||
|
# "monto": "1328.52"
|
||||||
|
# }
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': f'Bearer {token}',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/plain, */*'
|
||||||
|
}
|
||||||
|
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
response = requests.request("POST", url, headers=headers, json=payload, verify=False, timeout=30)
|
||||||
|
print(f" 📥 Status code: {response.status_code}")
|
||||||
|
return response.json()
|
||||||
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
||||||
|
print(f" ⚠️ Error de conexión (intento {attempt + 1}/{max_retries}): {e}")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time_to_wait = 2 ** (attempt + 1)
|
||||||
|
print(f" 🔄 Reintentando en {time_to_wait} segundos...")
|
||||||
|
time.sleep(time_to_wait)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
Reference in new issue
Block a user