import pyodbc from typing import List, Dict, Any, Optional, Tuple from logger_utils import log class GestorBaseDatos: def __init__(self, config: Dict[str, Any]): self.config = config self.conn = None self.cursor = None def conectar(self) -> bool: try: conn_str = ( f"DRIVER={{ODBC Driver 17 for SQL Server}};" f"SERVER={self.config['servidor']};" f"DATABASE={self.config['base_datos']};" f"UID={self.config['usuario']};" f"PWD={self.config['password']};" "Connection Timeout=30;" ) log.info(f"Conectando a SQL Server: {self.config['servidor']} - DB: {self.config['base_datos']}...") self.conn = pyodbc.connect(conn_str) log.info("Conexión SQL exitosa.") self.cursor = self.conn.cursor() return True except Exception as e: log.error(f"[DB ERROR] Fallo al conectar: {e}") return False def obtener_datos_pedimento(self, pedimento: str, sistema: str) -> Optional[Dict[str, Any]]: """Busca el pedimento usando coincidencia flexible (LIKE)""" tabla = "SPedimentos" if sistema == "SCAII" else "QPedimentos" query = f""" SELECT TIPO, REGIMEN, CLAVEPED, ADUANA_CRUCE, OPCIONDESTINO, TIPOPEDIMENTOTRANSPORTEE, TIPOPEDIMENTOTRANSPORTEA, TIPOPEDIMENTOTRANSPORTES, PEDIMENTO FROM {tabla} WHERE PEDIMENTO LIKE ? """ log.debug(f"Buscando pedimento: {pedimento} en {tabla}") try: self.cursor.execute(query, (f"%{pedimento.strip()}%",)) row = self.cursor.fetchone() if row: return { "Tipo": row[0], "Regimen": row[1], "Clave": row[2], "Aduana": row[3], "Destino": row[4], "TransE": row[5], "TransA": row[6], "TransS": row[7], "Pedimento": row[8] } return None except Exception as e: log.error(f"[DB ERROR] Error al obtener datos pedimento: {e}") return None def obtener_detalle_facturas(self, pedimento: str, sistema: str, regimen: str, tipo_op: str) -> List[Dict[str, Any]]: """Obtiene las facturas usando los querys maestros de WinDev""" if sistema == "SCAII": if tipo_op == "I": if regimen == "IMD": # Scenario: SComprasMexID (Importación Definitiva) query = f""" SELECT f.PedimentoImpoDef AS Pedimento, f.Consecutivo AS Consecutivo, f.FacCMImpoDef AS NumFactura, f.FechaFactura AS Fecha, f.Incoterm AS Incoterm, f.ValorImpoME AS ValorImpoME, f.Proveedor AS IdProveedor, f.ValorImpoMN AS ValorImpoMN, f.ClaveMoneda AS ClaveMoneda, f.ValorImpoMC AS ValorMC, f.MetValor AS MetodoValoracion, f.EsMixto AS EsMixto, f.Remesa AS Remesa, f.EDocument AS EDocument, f.NumeroCertificado AS NumCertificado, f.FirmaElectronica AS FirmaElectronica, f.NumeroNIU AS NumNIU, f.TipoDeGuiaAIdentificar AS TipoGuia, f.CantGuiasEmbarque AS CantGuias, f.ObservacionesVU AS Observaciones, f.ContenedoresTipo AS ContenedoresTipo, f.EnviadoA AS EnviadoA, f.FUNGECOMOCO AS FungeComoCO, f.SUBDIVISION AS Subdivision, f.TipoCambio AS TipoCambio, f.MODOCONTINGENCIA AS ModoContingencia, f.NUMOPERACIONVU AS NumOperacionVU, f.TRANSPORTISTA AS Transportista, f.NUMTRASPORTE AS NumTrailer, f.PESOBRUTO AS PesoBruto, f.FLETES AS Flete, f.SEGUROS AS Seguros, f.EMBALAJES AS Embalajes, f.OTROSINCREMENTA AS OtrosIncrementa, f.CANTBULTOS AS CantBultos, 'ME' AS TipoMoneda, p.VINCULACION AS Vinculacion, 0 AS IdCliente, '' AS CfdiUuid, '' AS CfdiPathPDF, '' AS CfdiPathXML FROM SComprasMexID f LEFT JOIN GClientesPro p ON f.PROVEEDOR = p.CLIENTE WHERE f.PEDIMENTOIMPODEF LIKE ? AND f.Estatus = 'AC' ORDER BY f.Remesa """ else: # Scenario: SFacImp (Importación Normal) query = f""" SELECT f.PedimentoImpo AS Pedimento, f.Consecutivo AS Consecutivo, f.FacturaImpo AS NumFactura, f.FechaFactura AS Fecha, f.Incoterm AS Incoterm, f.ValorImpoME AS ValorImpoME, f.Proveedor AS IdProveedor, f.ValorImpoMN AS ValorImpoMN, f.ClaveMoneda AS ClaveMoneda, f.ValorImpoMC AS ValorMC, f.MetValor AS MetodoValoracion, f.EsMixto AS EsMixto, f.Remesa AS Remesa, f.EDocument AS EDocument, f.NumeroCertificado AS NumCertificado, f.FirmaElectronica AS FirmaElectronica, f.NumeroNIU AS NumNIU, f.TipoDeGuiaAIdentificar AS TipoGuia, f.CantGuiasEmbarque AS CantGuias, f.ObservacionesVU AS Observaciones, f.ContenedoresTipo AS ContenedoresTipo, f.EnviadoA AS EnviadoA, f.FUNGECOMOCO AS FungeComoCO, f.SUBDIVISION AS Subdivision, f.TipoCambio AS TipoCambio, f.MODOCONTINGENCIA AS ModoContingencia, f.NUMOPERACIONVU AS NumOperacionVU, f.TRANSPORTISTA AS Transportista, f.NUMTRASPORTE AS NumTrailer, f.PESOBRUTO AS PesoBruto, f.FLETE AS Flete, f.SEGUROS AS Seguros, f.EMBALAJES AS Embalajes, f.OTROSINCREMENTA AS OtrosIncrementa, f.CANTBULTOS AS CantBultos, f.TIPOMONEDA AS TipoMoneda, p.VINCULACION AS Vinculacion, 0 AS IdCliente, '' AS CfdiUuid, '' AS CfdiPathPDF, '' AS CfdiPathXML FROM SFacImp f LEFT JOIN GClientesPro p ON f.PROVEEDOR = p.CLIENTE WHERE f.PedimentoImpo LIKE ? AND f.Estatus = 'AC' ORDER BY f.Remesa """ else: # Scenario: SFacExp (Exportación) query = f""" SELECT f.PedimentoExpo AS Pedimento, f.Consecutivo AS Consecutivo, f.FacturaExpo AS NumFactura, f.FechaFactura AS Fecha, f.Incoterm AS Incoterm, (SELECT SUM(MatPex.ValorTotalME) FROM SPartidasExpo MatPex LEFT JOIN SPartes Partes ON Partes.NumParte = MatPex.NumParte WHERE MatPex.Consecutivo = f.Consecutivo) AS ValorImpoME, f.Proveedor AS IdProveedor, (SELECT SUM(MatPex.ValorTotalMN) FROM SPartidasExpo MatPex LEFT JOIN SPartes Partes ON Partes.NumParte = MatPex.NumParte WHERE MatPex.Consecutivo = f.Consecutivo) AS ValorImpoMN, f.ClaveMoneda AS ClaveMoneda, 0 AS ValorMC, f.MetValor AS MetodoValoracion, 0 AS EsMixto, f.Remesa AS Remesa, f.EDocument AS EDocument, f.NumeroCertificado AS NumCertificado, f.FirmaElectronica AS FirmaElectronica, f.NumeroNIU AS NumNIU, f.TipoDeGuiaAIdentificar AS TipoGuia, f.CantGuiasEmbarque AS CantGuias, f.ObservacionesVU AS Observaciones, f.ContenedoresTipo AS ContenedoresTipo, f.EnviadoA AS EnviadoA, f.FUNGECOMOCO AS FungeComoCO, f.SUBDIVISION AS Subdivision, f.TipoCambio AS TipoCambio, f.MODOCONTINGENCIA AS ModoContingencia, f.NUMOPERACIONVU AS NumOperacionVU, f.TRANSPORTISTA AS Transportista, f.NUMTRASPORTE AS NumTrailer, f.PESOBRUTO AS PesoBruto, f.FLETE AS Flete, f.SEGUROS AS Seguros, f.EMBALAJES AS Embalajes, f.OTROSINCREMENTA AS OtrosIncrementa, f.CANTBULTOS AS CantBultos, f.TIPOMONEDA AS TipoMoneda, 0 AS Vinculacion, f.VendidoA AS IdCliente, f.CfdiUuid AS CfdiUuid, f.CfdiPathPDF AS CfdiPathPDF, f.CfdiPathXML AS CfdiPathXML FROM SFacExp f WHERE f.PedimentoExpo LIKE ? AND f.Estatus = 'AC' AND f.TipoFactura <> 'REPAR' ORDER BY f.Remesa """ else: # SCAF if tipo_op == "E": query = """ SELECT f.PedimentoExpo AS Pedimento, f.Consecutivo AS Consecutivo, f.FacturaExpo AS NumFactura, f.FechaFactura AS FechaFactura, f.Incoterm AS Incoterm, f.ValorExpoME AS ValorImpoME, f.Proveedor AS IdProveedor, f.ValorExpoMN AS ValorImpoMN, f.ClaveMoneda AS ClaveMoneda, f.MetValor AS MetValor, f.Remesa AS Remesa, f.EDocument AS EDocument, f.NumeroCertificado AS NumeroCertificado, f.FirmaElectronica AS FirmaElectronica, f.NUMERONIU AS NumNIU, f.TIPODEGUIAAIDENTIFICAR AS TipoGuia, f.CANTGUIASEMBARQUE AS CantGuias, f.OBSERVACIONESVU AS ObservacionesVU, f.CONTENEDORESTIPO AS ContenedoresTipo, f.FUNGIRCOMOCO AS FUNGECOMOCO, f.SUBDIVISION AS SUBDIVISION, f.TIPOCAMBIO AS TipoCambio, f.MODOCONTINGENCIA AS MODOCONTINGENCIA, f.NUMOPERACIONVU AS NUMOPERACIONVU, f.TRANSPORTISTA AS TRANSPORTISTA, f.CANTBULTOS AS CANTBULTOS, f.TIPOMONEDA AS TipoMoneda, (SELECT SUM(PesoBrutoKGS) FROM QEqeMaq WHERE Consecutivo = f.Consecutivo) AS PesoBruto FROM QFacExp f WHERE f.PedimentoExpo LIKE ? AND f.Estatus = 'AC' ORDER BY f.Remesa """ else: # SCAF Importación tabla_partidas = "QEqiDef" if regimen == "IMD" else "QEqiMaq" query = f""" SELECT f.PedimentoImpo AS Pedimento, f.Consecutivo AS Consecutivo, f.FacturaImpo AS NumFactura, f.FechaFactura AS FechaFactura, f.Incoterm AS Incoterm, f.ValorImpoME AS ValorImpoME, f.Proveedor AS IdProveedor, f.ValorImpoMN AS ValorImpoMN, f.ClaveMoneda AS ClaveMoneda, f.MetValor AS MetValor, f.Remesa AS Remesa, f.EDocument AS EDocument, f.NumeroCertificado AS NumeroCertificado, f.FirmaElectronica AS FirmaElectronica, f.NUMERONIU AS NumNIU, f.TIPODEGUIAAIDENTIFICAR AS TipoGuia, f.CANTGUIASEMBARQUE AS CantGuias, f.OBSERVACIONESVU AS ObservacionesVU, f.CONTENEDORESTIPO AS ContenedoresTipo, f.FUNGIRCOMOCO AS FUNGECOMOCO, f.SUBDIVISION AS SUBDIVISION, f.TIPOCAMBIO AS TipoCambio, f.MODOCONTINGENCIA AS MODOCONTINGENCIA, f.NUMOPERACIONVU AS NUMOPERACIONVU, f.TRANSPORTISTA AS TRANSPORTISTA, f.CANTBULTOS AS CANTBULTOS, f.TIPOMONEDA AS TipoMoneda, (SELECT SUM(PesoBrutoKGS) FROM {tabla_partidas} WHERE Consecutivo = f.Consecutivo) AS PesoBruto FROM QFacImp f WHERE f.PedimentoImpo LIKE ? AND f.Estatus = 'AC' ORDER BY f.Remesa """ try: self.cursor.execute(query, (f"%{pedimento.strip()}%",)) columns = [column[0] for column in self.cursor.description] return [dict(zip(columns, row)) for row in self.cursor.fetchall()] except Exception as e: log.error(f"[DB ERROR] Error al obtener facturas: {e}") return [] def obtener_partidas_factura(self, consecutivo_fac: int, sistema: str, tipo_op: str, regimen: str) -> List[Dict[str, Any]]: """Querys homologados de partidas""" if sistema == "SCAII": if tipo_op == "I": tabla = "SPartidasCM" if regimen == "IMD" else "SPartidasImpo" col_linea = "LineaCMImpDef" if regimen == "IMD" else "LineaImpo" col_fac = "FACCMIMPODEF" if regimen == "IMD" else "FACTURAIMPO" query = f""" SELECT p.Consecutivo AS Consecutivo, p.{col_linea} AS Linea, p.NumParte AS NumParte, p.CantImpo AS Cantidad, p.ValorImpoMC AS ValorComercial, p.UniMed AS UnidadMedida, p.FraccionImpo AS Fraccion, p.PesoNetoKGS AS PesoNeto, p.TipoFraccImpo AS TipoFracc, p.CostoUnitarioME AS CostoUnitarioME, p.ValorImpoME AS ValorDolares, p.ValorImpoMN AS ValorPesos, p.PaisOrigen AS PaisOrigen, p.Sector AS Sector, p.PesoBrutoKGS AS PesoBruto, p.ClaveBultos AS ClaveBultos, p.CantBultos AS CantBultos, p.InfoAdicionEsp AS InfoAdic, p.Clase AS Clase, (SELECT TOP 1 UMCLAVE FROM sfracciones WHERE fraccion = SUBSTRING(p.FraccionImpo, 1, 8)) AS ClaveOMA, p.FraccionROctava AS FraccionOctava, p.DESCRIPCIONPARTE AS Descripcion, p.{col_fac} AS NumFactura, p.CANTALTERNA AS CantidadAlterna, p.UNIMEDALTERNA AS UnidadAlterna, p.VALORADUANASMN AS ValorAduanasMN, p.VALORADUANASME AS ValorAduanasME, ISNULL(sp.DESCRIPCIONI, p.INFOADICIONING) AS DescripcionIngles, 0 AS ValorAgregadoMN, p.METVALOR AS MetodoValoracion, p.COSTOUAUXILIARME AS CostoAuxiliarME, '' AS Marca, '' AS Modelo, 0 AS EsSubPartida FROM {tabla} p LEFT JOIN SPartes sp ON p.NumParte = sp.NUMPARTE WHERE p.Consecutivo = ? ORDER BY p.{col_linea} """ else: # EXPO SCAII query = f""" SELECT p.FacturaExpo AS Consecutivo, p.Linea AS Linea, p.NumParte AS NumParte, p.CantExpo AS Cantidad, p.ValorTotalMN AS ValorComercial, p.UniMed AS UnidadMedida, p.FraccionExpo AS Fraccion, p.PesoNetoKGS AS PesoNeto, p.TipoFracExpo AS TipoFracc, p.CostoUnitarioME AS CostoUnitarioME, p.ValorTotalME AS ValorDolares, p.ValorTotalMN AS ValorPesos, p.PaisOrigen AS PaisOrigen, p.Sector AS Sector, p.PesoBrutoKGS AS PesoBruto, p.ClaveBultos AS ClaveBultos, p.CantBultos AS CantBultos, p.InfoAdicionEsp AS InfoAdic, p.Clase AS Clase, (SELECT TOP 1 UMCLAVE FROM sfracciones WHERE fraccion = SUBSTRING(p.FraccionExpo, 1, 8)) AS ClaveOMA, '' AS FraccionOctava, p.DESCRIPCIONPARTE AS Descripcion, p.FACTURAEXPO AS NumFactura, 0 AS CantidadAlterna, '' AS UnidadAlterna, p.VALORADUANASMN AS ValorAduanasMN, p.ValorAgreME AS ValorAgregadoME, p.ValorAgreMN AS ValorAgregadoMN, p.MetValor AS MetodoValoracion, '' AS Marca, '' AS Modelo, 0 AS EsSubPartida, ISNULL(sp.DESCRIPCIONI, p.DESCRIPCIONPARTE) AS DescripcionIngles, 0 AS CostoAuxiliarME FROM SPartidasExpo p LEFT JOIN SPartes sp ON p.NumParte = sp.NUMPARTE WHERE Consecutivo = ? ORDER BY p.LINEA """ else: # SCAF if tipo_op == "I": tabla = "QEqiDef" if regimen == "IMD" else ("QEqiMaqRep" if "REP" in regimen else "QEqiMaq") col_linea = "LineaImpoDef" if regimen == "IMD" else "LineaImpo" col_fac = "FACTURAIMPODEF" if regimen == "IMD" else "FACTURAIMPO" col_cant = "CantImpoDef" if regimen == "IMD" else "CantImpo" col_vme = "ValorME" if regimen == "IMD" else "ValorImpoME" col_vmn = "ValorMN" if regimen == "IMD" else "ValorImpoMN" query = f""" SELECT p.Consecutivo AS Consecutivo, p.{col_linea} AS Linea, p.NumParte AS NumParte, p.{col_cant} AS Cantidad, p.ValorImpoMC AS ValorComercial, p.UnidadMedida AS UnidadMedida, p.Fraccion AS Fraccion, p.PesoNetoKGS AS PesoNeto, p.TipoFraccion AS TipoFracc, p.CostoUnitarioDlls AS CostoUnitarioME, p.{col_vme} AS ValorDolares, p.{col_vmn} AS ValorPesos, p.PaisOrigen AS PaisOrigen, p.Sector AS Sector, p.PesoBrutoKGS AS PesoBruto, p.ClaveBultos AS ClaveBultos, p.CantBultos AS CantBultos, p.DescripcionE AS InfoAdic, p.Clase AS Clase, (SELECT TOP 1 UMCLAVE FROM sfracciones WHERE fraccion = SUBSTRING(p.Fraccion, 1, 8)) AS ClaveOMA, p.FRACCIONROCTAVA AS FraccionOctava, ISNULL(c.DESCRIPCIONE, p.DESCRIPCIONE) AS Descripcion, p.{col_fac} AS NumFactura, 0 AS CantidadAlterna, ' ' AS UnidadAlterna, p.ValorAduanasMN AS ValorAduanasMN, p.ValorAduanasME AS ValorAduanasME, 0 AS ValorAgregadoMN, p.METVALOR AS MetodoValoracion, p.Marca AS Marca, p.Modelo AS Modelo, p.EsSubPartida AS EsSubPartida, ISNULL(c.DESCRIPCIONI, p.DESCRIPCIONE) AS DescripcionIngles, 0 AS CostoAuxiliarME FROM {tabla} p LEFT JOIN QClaAct c ON c.CLASE = p.NumParte WHERE p.Consecutivo = ? ORDER BY p.{col_linea} """ else: # EXPO SCAF query = """ SELECT p.Consecutivo AS Consecutivo, p.LineaExpo AS Linea, p.FacturaImpo AS NumParte, p.CantExpo AS Cantidad, 0 AS ValorComercial, p.UnidadMedida AS UnidadMedida, p.FraccionExpo AS Fraccion, p.PesoNetoKGS AS PesoNeto, p.TipoFraccion AS TipoFracc, p.CostoUnitarioDlls AS CostoUnitarioME, p.ValorExpoME AS ValorDolares, p.ValorExpoMN AS ValorPesos, p.PaisOrigen AS PaisOrigen, p.Sector AS Sector, p.PesoBrutoKGS AS PesoBruto, p.ClaveBultos AS ClaveBultos, p.CantBultos AS CantBultos, p.DescripcionE AS InfoAdic, p.Clase AS Clase, (SELECT TOP 1 UMCLAVE FROM sfracciones WHERE fraccion = SUBSTRING(p.FraccionExpo, 1, 8)) AS ClaveOMA, ' ' AS FraccionOctava, ISNULL(c.DESCRIPCIONE, p.DESCRIPCIONE) AS Descripcion, p.FACTURAEXPO AS NumFactura, 0 AS CantidadAlterna, ' ' AS UnidadAlterna, p.VALORADUANASMN AS ValorAduanasMN, 0 AS ValorAgregadoME, 0 AS ValorAgregadoMN, p.MetValor AS MetodoValoracion, p.Marca AS Marca, p.Modelo AS Modelo, p.ESSUBPARTIDA AS EsSubPartida, ISNULL(c.DESCRIPCIONI, p.DESCRIPCIONE) AS DescripcionIngles, 0 AS CostoAuxiliarME FROM QEqeMaq p LEFT JOIN QClaAct c ON c.CLASE = p.FacturaImpo WHERE p.Consecutivo = ? ORDER BY p.LineaExpo """ try: self.cursor.execute(query, (consecutivo_fac,)) columns = [column[0] for column in self.cursor.description] return [dict(zip(columns, row)) for row in self.cursor.fetchall()] except Exception as e: print(f"[DB ERROR] Error al obtener partidas: {e}") return [] def obtener_total_vehiculos(self, pedimento: str, sistema: str) -> int: """Calcula el total de vehículos (Trailers)""" if sistema == "SCAII": query = """ SELECT DISTINCT MatFex.NUMTRAILER FROM SPartidasExpo MatPex LEFT OUTER JOIN SFacExp MatFex ON MatFex.Consecutivo = MatPex.Consecutivo WHERE (MatFex.PedimentoExpo LIKE ?) AND MatFex.Estatus = 'AC' """ try: self.cursor.execute(query, (f"%{pedimento.strip()}%",)) rows = self.cursor.fetchall() return len(rows) except: return 0 else: # SCAF # En SCAF buscamos en las tablas de partidas (QEqiMaq/QEqeMaq/etc) # Primero intentamos obtener el tipo de pedimento para saber qué tabla usar datos_pedi = self.obtener_datos_pedimento(pedimento, sistema) if not datos_pedi: return 0 tipo_op = datos_pedi.get("Tipo") regimen = datos_pedi.get("Regimen", "") if tipo_op == "E": tabla = "QEqeMaq" else: tabla = "QEqiDef" if regimen == "IMD" else "QEqiMaq" query = f"SELECT COUNT(DISTINCT NUMTRAILER) FROM {tabla} WHERE PEDIMENTO LIKE ?" try: self.cursor.execute(query, (f"%{pedimento.strip()}%",)) row = self.cursor.fetchone() return int(row[0]) if row else 0 except: return 0 return 0 def obtener_datos_empresa(self, sistema: str = "SCAII") -> Optional[Dict[str, Any]]: # Casi todos los sistemas (SCAII/SCAF) usan GEmpresa como tabla global tabla = "GEmpresa" try: self.cursor.execute(f"SELECT TOP 1 NOMBRE, RFC, NUMDEEXPORTADORCONFIABLE FROM {tabla}") row = self.cursor.fetchone() if row: return {"NOMBRE": row[0], "RFC": row[1], "NUMDEEXPORTADORCONFIABLE": row[2] if len(row)>2 else ""} return None except Exception as e: print(f"[DB ERROR] Error al obtener datos de empresa: {e}") return None def obtener_credenciales_rpa(self) -> tuple: """Lee usuario/password del API desde GEmpresa, igual que WinDev en produccion. Usa los campos NOMBREBDINTER y CLAVEPREVALIDADOR.""" try: self.cursor.execute("SELECT TOP 1 NOMBREBDINTER, CLAVEPREVALIDADOR FROM GEmpresa") row = self.cursor.fetchone() if row and row[0] and row[1]: log.info(f"Credenciales RPA obtenidas de GEmpresa (usuario: {row[0]})") return str(row[0]).strip(), str(row[1]).strip() else: log.warning("GEmpresa no tiene NOMBREBDINTER/CLAVEPREVALIDADOR configurados.") return "", "" except Exception as e: log.error(f"[DB ERROR] Error al obtener credenciales RPA de GEmpresa: {e}") return "", "" def obtener_datos_proveedor(self, id_proveedor: int, sistema: str) -> Optional[Dict[str, Any]]: if sistema == "SCAII": tabla = "GClientesPro" query = f"SELECT NOMBRE, PAIS, TAXID, VINCULACION, CLAVETRANSFER FROM {tabla} WHERE CLIENTE = ?" else: tabla = "GClientesPro" query = f"SELECT NOMBRE, PAIS, TAXID, VINCULACION, CLAVETRANSFER FROM {tabla} WHERE CLIENTE = ?" try: self.cursor.execute(query, (id_proveedor,)) row = self.cursor.fetchone() if row: return { "NOMBRE": row[0], "PAIS": row[1], "TAXID": row[2], "VINCULACION": row[3], "CLAVETRANSFER": row[4] if len(row) > 4 else "" } return None except Exception as e: print(f"[DB ERROR] Error al obtener proveedor: {e}") return None def obtener_datos_transporte(self, clave_transporte: str, sistema: str = "SCAII") -> Optional[Dict[str, Any]]: if not clave_transporte: return None if sistema == "SCAII": tabla = "GTransportista" query = f"SELECT RFC, CODIGOCAAT, PAIS FROM {tabla} WHERE CLAVETRANS = ?" else: tabla = "GTransportista" # Confirmado por WinDev query = f"SELECT RFC, CODIGOCAAT, PAIS FROM {tabla} WHERE CLAVETRANS = ?" try: self.cursor.execute(query, (clave_transporte,)) row = self.cursor.fetchone() if row: return {"RFC": row[0], "CODIGOCAAT": row[1], "PAIS": row[2]} return None except Exception as e: print(f"[DB ERROR] Error al obtener transporte: {e}") return None def obtener_datos_trailer(self, num_trailer: str, sistema: str = "SCAII") -> Optional[Dict[str, Any]]: if not num_trailer: return None if sistema == "SCAII": tabla = "GTrailers" query = f"SELECT NUMEROPLACAS, PAIS FROM {tabla} WHERE NUMTRAILER = ?" else: tabla = "GTrailers" # Confirmado por WinDev query = f"SELECT NUMEROPLACAS, PAIS FROM {tabla} WHERE NUMTRAILER = ?" try: self.cursor.execute(query, (num_trailer,)) row = self.cursor.fetchone() if row: return {"NUMEROPLACAS": row[0], "PAIS": row[1]} return None except Exception as e: print(f"[DB ERROR] Error al obtener trailer: {e}") return None def obtener_um_tarifa_fraccion(self, fraccion: str, sistema: str = "SCAII") -> Optional[str]: if not fraccion: return None fraccion_corta = str(fraccion)[:8].replace(".", "") tabla = "SFracciones" if sistema == "SCAII" else "QFracciones" try: self.cursor.execute(f"SELECT TOP 1 UMCLAVE FROM {tabla} WHERE FRACCION LIKE ?", (fraccion_corta + '%',)) row = self.cursor.fetchone() return str(row[0]).strip() if row else None except Exception as e: print(f"[DB ERROR] Error UM Tarifa: {e}") return None def obtener_unidad_aduana_scaii(self, clave_aduana: str) -> Optional[str]: if not clave_aduana: return None try: self.cursor.execute("SELECT TOP 1 CLAVEUNI FROM GUniMedida WHERE CLAVEOMA = ?", (clave_aduana,)) row = self.cursor.fetchone() return str(row[0]).strip() if row else None except Exception as e: return None def obtener_conversion_unidades(self, um_origen: str, um_destino: str) -> Tuple[str, float]: if not um_origen or not um_destino or um_origen == um_destino: return "M", 1.0 try: self.cursor.execute("SELECT FACTORCONV FROM GConversiones WHERE CLAVEUNI1 = ? AND CLAVEUNI2 = ?", (um_origen, um_destino)) row = self.cursor.fetchone() if row: return "M", float(row[0]) self.cursor.execute("SELECT FACTORCONV FROM GConversiones WHERE CLAVEUNI1 = ? AND CLAVEUNI2 = ?", (um_destino, um_origen)) row = self.cursor.fetchone() if row: return "D", float(row[0]) return "M", 1.0 except: return "M", 1.0 def obtener_identificadores_partida(self, consecutivo: int, linea: int, modulo: str) -> List[Dict[str, Any]]: if not consecutivo or not linea or not modulo: return [] query = """ SELECT ID, COMPLEMENTO1, COMPLEMENTO2, COMPLEMENTO3 FROM GIdentificadoresPartidas WHERE CONSECUTIVOFACTURA = ? AND LINEAPARTIDA = ? AND MODULO = ? """ try: self.cursor.execute(query, (consecutivo, linea, modulo)) cols = [column[0] for column in self.cursor.description] return [dict(zip(cols, row)) for row in self.cursor.fetchall()] except Exception as e: print(f"[DB ERROR] Error al obtener identificadores de partida: {e}") return [] def obtener_datos_unidad_medida(self, clave_raw: str, sistema: str) -> Optional[Dict[str, Any]]: tabla = "Sunidades" if sistema == "SCAII" else "Qunidades" query = f"SELECT CLAVE, DESCRIPCION, CLAVEOMA FROM {tabla} WHERE CLAVE = ? OR DESCRIPCION = ?" try: self.cursor.execute(query, (clave_raw, clave_raw)) row = self.cursor.fetchone() if row: return {"CLAVE": row[0], "DESCRIPCION": row[1], "CLAVEOMA": row[2] if len(row) > 2 else " "} return None except Exception as e: print(f"[DB ERROR] Error al obtener unidad: {e}") return None def cerrar(self): if self.conn: self.conn.close()