chore: initial commit del proyecto SCAII_Sync_Client

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 13:38:20 -05:00
commit 10dc063cf1
26 changed files with 2069 additions and 0 deletions

218
motor_sincronizacion.py Normal file
View File

@@ -0,0 +1,218 @@
import time
import os
from typing import List, Dict, Any, Tuple, Optional
from registros.scaii.reg_501 import Generador501 as Gen501SCAII
from registros.scaf.reg_501 import Generador501SCAF as Gen501SCAF
from registros.scaii.reg_502 import Generador502SCAII as Gen502SCAII
from registros.scaf.reg_502 import Generador502SCAF as Gen502SCAF
from registros.scaii.reg_503 import Generador503SCAII as Gen503SCAII
from registros.scaf.reg_503 import Generador503SCAF as Gen503SCAF
from registros.scaii.reg_504 import Generador504SCAII as Gen504SCAII
from registros.scaf.reg_504 import Generador504SCAF as Gen504SCAF
from registros.scaii.reg_505 import Generador505SCAII as Gen505SCAII
from registros.scaf.reg_505 import Generador505SCAF as Gen505SCAF
from registros.scaii.reg_551 import Generador551SCAII as Gen551SCAII
from registros.scaf.reg_551 import Generador551SCAF as Gen551SCAF
from registros.scaii.reg_554 import Generador554SCAII as Gen554SCAII
from registros.scaf.reg_554 import Generador554SCAF as Gen554SCAF
from registros.scaii.reg_558 import Generador558 as Gen558SCAII
from registros.scaf.reg_558 import Generador558SCAF as Gen558SCAF
from logger_utils import log
class MotorSincronizacion:
def __init__(self, gestor_db, cliente_api):
self.db = gestor_db
self.api = cliente_api
self.tipo_moneda_global = "ME"
self.callback_progreso = None
self.callback_fila = None
self.callback_estado_texto = None
self.callback_actualizar_fila = None
def proceso_principal(self):
"""Metodo de entrada robusto para la Interfaz"""
config_datos = self.db.config
pedimento = config_datos.get("pedimento")
sistema = config_datos.get("sistema", "SCAII")
log.info(f"Iniciando proceso principal para Pedimento: {pedimento}, Sistema: {sistema}")
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto("Iniciando conexiones...")
# 1. Conectar a la Base de Datos
if not self.db.conectar():
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto("Error: Fallo conexión SQL Server.")
return False
# 1b. Leer credenciales RPA de GEmpresa (igual que WinDev en producción)
# Si vienen por parámetro (/RPAUser), se usan esas. Si no, se leen de la BD.
if not self.api.usuario or not self.api.password:
rpa_user, rpa_pass = self.db.obtener_credenciales_rpa()
if rpa_user and rpa_pass:
self.api.usuario = rpa_user
self.api.password = rpa_pass
else:
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto("Error: No hay credenciales RPA en GEmpresa.")
log.error("No se encontraron credenciales RPA en GEmpresa. Configura NOMBREBDINTER y CLAVEPREVALIDADOR.")
return False
# 2. Obtener Token de la API
if not self.api.obtener_token():
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto("Error: Fallo de autenticación API.")
return False
if not pedimento: return False
return self.ejecutar_logica_generacion(pedimento, sistema)
def ejecutar_logica_generacion(self, pedimento: str, sistema: str) -> bool:
"""Orquestador Gen_Regs"""
# 1. Buscar pedimento (Intento 1: Tal cual viene)
datos_pedi = self.db.obtener_datos_pedimento(pedimento, sistema)
# 2. Buscar pedimento (Intento 2: Sin guiones)
if not datos_pedi and "-" in pedimento:
log.info("Intento 2: Buscando pedimento sin guiones...")
datos_pedi = self.db.obtener_datos_pedimento(pedimento.replace("-", ""), sistema)
# 3. Buscar pedimento (Intento 3: Solo los últimos 7 dígitos - El número consecutivo)
if not datos_pedi and len(pedimento) >= 7:
solo_consecutivo = pedimento.replace("-", "").replace(" ", "")[-7:]
log.info(f"Intento 3: Buscando solo por consecutivo final: {solo_consecutivo}...")
datos_pedi = self.db.obtener_datos_pedimento(solo_consecutivo, sistema)
if not datos_pedi:
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto(f"Error: Pedimento {pedimento} no encontrado.")
return False
tipo_op = datos_pedi.get("Tipo")
regimen = datos_pedi.get("Regimen", "")
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto(f"Pedimento {tipo_op}/{regimen} encontrado. Buscando facturas...")
log.info(f"Pedimento encontrado: {tipo_op}/{regimen}. Buscando facturas...")
datos_empresa = self.db.obtener_datos_empresa(sistema)
facturas = self.db.obtener_detalle_facturas(pedimento, sistema, regimen, tipo_op)
if not facturas:
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto(f"Aviso: 0 facturas para {pedimento}")
return False
self.facturas_cache = facturas
totales = self.calcular_totales(facturas, self.tipo_moneda_global)
id_proveedor = facturas[0].get("IdProveedor")
datos_cliente = self.db.obtener_datos_proveedor(id_proveedor, sistema)
total_trailers = self.db.obtener_total_vehiculos(pedimento, sistema)
gen_501 = Gen501SCAII if sistema == "SCAII" else Gen501SCAF
json_501 = gen_501.preparar_json(datos_pedi, totales, datos_cliente, total_trailers)
id_pedimento_api = self.api.enviar_registro_api("registro501", json_501)
if not id_pedimento_api: return False
num_trailer = facturas[0].get("NumTrailer")
datos_trailer = self.db.obtener_datos_trailer(num_trailer, sistema)
gen_502 = Gen502SCAII if sistema == "SCAII" else Gen502SCAF
self.api.enviar_registro_api("registro502", gen_502.preparar_json(id_pedimento_api, datos_trailer))
gen_503 = Gen503SCAII if sistema == "SCAII" else Gen503SCAF
self.api.enviar_registro_api("registro503", gen_503.preparar_json(id_pedimento_api, facturas[0]))
log.info(f"Procesando {len(facturas)} facturas...")
for idx, fac in enumerate(facturas):
self.idx_actual = idx
if self.callback_fila:
self.callback_fila(pedimento, str(fac.get("NumFactura")), str(fac.get("Remesa")), fac, "Procesando...")
time.sleep(0.02)
gen_504 = Gen504SCAII if sistema == "SCAII" else Gen504SCAF
for j504 in gen_504.preparar_listado_json(id_pedimento_api, fac.get("ContenedoresTipo")):
self.api.enviar_registro_api("registro504", j504)
id_prov = fac.get("IdProveedor")
prov_data = self.db.obtener_datos_proveedor(id_prov, sistema)
trans_data = self.db.obtener_datos_transporte(fac.get("Transportista"), sistema)
gen_505 = Gen505SCAII if sistema == "SCAII" else Gen505SCAF
json_505 = gen_505.preparar_json(fac, id_pedimento_api, datos_empresa, prov_data, trans_data, self.tipo_moneda_global, 1.0)
json_505["tipo_operacion_505"] = tipo_op
id_factura_api = self.api.enviar_registro_api("registro505", json_505)
if id_factura_api:
self.ejecutar_generacion_partidas(fac, id_factura_api, sistema, tipo_op, regimen, json_505)
if getattr(self, "callback_actualizar_fila", None):
self.callback_actualizar_fila(idx, "Enviada correctamente")
else:
if getattr(self, "callback_actualizar_fila", None):
self.callback_actualizar_fila(idx, "Error enviando")
if getattr(self, "callback_progreso", None):
self.callback_progreso((idx + 1) / len(facturas))
time.sleep(0.05)
log.info("Sincronización finalizada con éxito.")
return True
def calcular_totales(self, facturas: List[Dict[str, Any]], moneda_objetivo: str) -> Dict[str, float]:
totales = {"peso_bruto": 0.0, "bultos": 0.0, "flete": 0.0, "seguros": 0.0, "embalajes": 0.0, "otros_incrementa": 0.0}
for fac in facturas:
totales["peso_bruto"] += float(fac.get("PesoBruto") or 0)
totales["bultos"] += float(fac.get("CANTBULTOS") or 0)
tc = float(fac.get("TipoCambio") or 1.0)
m_fac = fac.get("ClaveMoneda")
def conv(v):
val = float(v or 0)
if moneda_objetivo == "ME": return val if m_fac == "ME" else val / tc
return val * tc if m_fac == "MN" else val
totales["flete"] += conv(fac.get("Flete"))
totales["seguros"] += conv(fac.get("Seguros"))
totales["embalajes"] += conv(fac.get("Embalajes"))
totales["otros_incrementa"] += conv(fac.get("OtrosIncrementa"))
return totales
def realizar_conversion_tarifa(self, cant_c: float, um_c: str, frac: str, peso: float, sist: str) -> Tuple[float, str]:
cl_um = self.db.obtener_um_tarifa_fraccion(frac, sist)
if not cl_um: return cant_c, ""
if str(cl_um) == "1": return peso, "1"
um_s = self.db.obtener_unidad_aduana_scaii(clave_aduana=cl_um)
if not um_s or um_s == um_c: return cant_c, str(cl_um)
t, f = self.db.obtener_conversion_unidades(um_c, um_s)
cant_t = cant_c * f if t == "M" else (cant_c / f if f != 0 else cant_c)
return cant_t, str(cl_um)
def ejecutar_generacion_partidas(self, fac: Dict[str, Any], id_f: int, sist: str, t_op: str, reg: str, j505: Dict[str, Any]):
partidas = self.db.obtener_partidas_factura(fac.get("Consecutivo"), sist, t_op, reg)
if not partidas: return
for i, par in enumerate(partidas, start=1):
if getattr(self, "callback_estado_texto", None):
self.callback_estado_texto(f"Enviando Partida {i}/{len(partidas)}...")
# Obtener datos de la unidad de medida para el mapeo numérico
um_raw = par.get("UnidadMedida", "")
um_data = self.db.obtener_datos_unidad_medida(um_raw, sist) if um_raw else None
ct, ut = self.realizar_conversion_tarifa(float(par.get("Cantidad") or 0), par.get("UnidadMedida", ""), par.get("Fraccion", ""), float(par.get("PesoNeto") or 0), sist)
if sist == "SCAII":
j551 = Gen551SCAII.preparar_json(par, id_f, {}, um_data, str(fac.get("MetodoValoracion") or ""), j505["moneda_facturacion_505"], fac.get("Remesa"), i, fac.get("TipoMoneda", "ME"))
else:
j551 = Gen551SCAF.preparar_json(par, id_f, {}, um_data, str(fac.get("MetodoValoracion") or ""), j505["moneda_facturacion_505"], fac.get("Remesa"), i)
j551["cantidad_tarifa_551"] = ct
if ut: j551["unidad_medida_tarifa_551"] = ut
j551["numero_partida_remesa_551"] = str(par.get("Linea", i))
id_p = self.api.enviar_registro_api("registro551", j551)
if id_p:
mod = ("SCAII" if sist == "SCAII" else "SCAF") + ("-IT" if t_op == "I" and reg != "IMD" else ("-ID" if t_op == "I" else ("-E" if sist == "SCAII" else "-ER")))
for iden in self.db.obtener_identificadores_partida(fac.get("Consecutivo"), par.get("Linea"), mod):
self.api.enviar_registro_api("registro554", (Gen554SCAII if sist == "SCAII" else Gen554SCAF).preparar_json(iden, id_p))
if par.get("InfoAdic", "").strip():
self.api.enviar_registro_api("registro558", (Gen558SCAII if sist == "SCAII" else Gen558SCAF).preparar_json(id_p, par.get("InfoAdic")))
time.sleep(0.01)