chore: initial commit del proyecto SCAII_Sync_Client
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
venv/
|
||||
build/
|
||||
dist/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.spec.bak
|
||||
39
SCAII_Sincronizador_32bit.spec
Normal file
39
SCAII_Sincronizador_32bit.spec
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='SCAII_Sincronizador_32bit',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
version='C:\\Users\\PC\\AppData\\Local\\Temp\\6dd1fff1-457d-40b5-9d54-2ef8d8c29463',
|
||||
)
|
||||
118
cliente_api.py
Normal file
118
cliente_api.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from logger_utils import log
|
||||
|
||||
class ClienteAPI:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.url_api = config.get("url_api", "").rstrip('/') + '/'
|
||||
self.usuario = config.get("usuario_rpa", "")
|
||||
self.password = config.get("password_rpa", "")
|
||||
self.sistema = config.get("sistema", "SCAII")
|
||||
self.token = None
|
||||
|
||||
def obtener_token(self) -> bool:
|
||||
"""
|
||||
Equivalente a GetTokenAPI() de WinDev.
|
||||
Realiza el login y guarda el token de sesión.
|
||||
"""
|
||||
log.info(f"Intentando login API para sistema {self.sistema} en {self.url_api}...")
|
||||
|
||||
# Lógica de endpoint basada en WinDev (Solo SCAII o SCAF)
|
||||
endpoint = "login"
|
||||
|
||||
url = f"{self.url_api}{endpoint}"
|
||||
payload = {
|
||||
"username": self.usuario,
|
||||
"password": self.password
|
||||
}
|
||||
|
||||
try:
|
||||
# En WinDev: RequestAPI.IgnoreError = httpIgnoreInvalidCertificate
|
||||
response = requests.post(url, json=payload, verify=False, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Extracción de token (Solo SCAII o SCAF)
|
||||
self.token = data.get("token")
|
||||
|
||||
if self.token:
|
||||
log.info("Login API exitoso. Token obtenido.")
|
||||
return True
|
||||
else:
|
||||
log.error("Respuesta de login OK pero no se encontró el token.")
|
||||
return False
|
||||
else:
|
||||
log.error(f"Error de autenticación API ({response.status_code}): {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Excepción en login API: {e}")
|
||||
return False
|
||||
|
||||
def actualizar_estatus_factura(self, numero_factura: str) -> bool:
|
||||
"""Actualiza el estatus de una factura en la nube"""
|
||||
if not self.token:
|
||||
return False
|
||||
|
||||
url = f"{self.url_api}registro505/descargar/{numero_factura}"
|
||||
headers = {"Authorization": f"Bearer {self.token}"}
|
||||
|
||||
try:
|
||||
# Esta es una llamada de ejemplo, ajustar según necesidad real
|
||||
# response = requests.get(url, headers=headers, verify=False)
|
||||
print(f"[API] Estatus de factura {numero_factura} actualizado.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._guardar_log_error(f"Error factura {numero_factura}: {e}")
|
||||
return False
|
||||
|
||||
def enviar_registro_api(self, endpoint: str, payload: dict) -> Optional[int]:
|
||||
"""Método genérico para enviar cualquier registro (501, 505, 551, etc.)"""
|
||||
if not self.token:
|
||||
if not self.obtener_token():
|
||||
return None
|
||||
|
||||
url = f"{self.url_api}{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
log.debug(f"API POST -> {url}")
|
||||
log.debug(f"PAYLOAD -> {json.dumps(payload)}")
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers, verify=False, timeout=20)
|
||||
|
||||
log.debug(f"API STATUS <- {response.status_code}")
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.json()
|
||||
new_id = (data.get("id") or
|
||||
data.get("id_pedimento") or
|
||||
data.get("id_factura") or
|
||||
data.get("id_partida") or
|
||||
data.get("id_creado") or
|
||||
data.get("id_registro"))
|
||||
log.info(f"Registro enviado con éxito a {endpoint}. ID obtenido: {new_id}")
|
||||
return new_id
|
||||
else:
|
||||
log.error(f"Error API enviando {endpoint} ({response.status_code}): {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Excepción enviando registro a {endpoint}: {e}")
|
||||
return None
|
||||
|
||||
def _guardar_log_error(self, mensaje: str):
|
||||
fecha = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
nombre_archivo = f"Error_API_{fecha}.txt"
|
||||
try:
|
||||
with open(nombre_archivo, "w") as f:
|
||||
f.write(mensaje)
|
||||
print(f"[ERROR] Log guardado en {nombre_archivo}")
|
||||
except:
|
||||
pass
|
||||
117
config.py
Normal file
117
config.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Dict, Any
|
||||
from logger_utils import log
|
||||
|
||||
class ConfiguracionApp:
|
||||
def __init__(self):
|
||||
self.datos: Dict[str, Any] = {
|
||||
"servidor": "",
|
||||
"base_datos": "",
|
||||
"usuario": "sa",
|
||||
"password": "",
|
||||
"puerto": 0,
|
||||
"sistema": "SCAF",
|
||||
"pedimento": "",
|
||||
"tipo_moneda": "ME",
|
||||
"proceso": 1,
|
||||
"url_api": "", # Se calculará dinámicamente
|
||||
"progreso_actual": 0
|
||||
}
|
||||
self.usuario_rpa = ""
|
||||
self.password_rpa = ""
|
||||
|
||||
def cargar(self):
|
||||
log.info("Iniciando carga de configuración (Modo Parámetros)...")
|
||||
log.info(f"=== ARGUMENTOS RECIBIDOS (sys.argv) ===")
|
||||
log.info(f"Total de argumentos: {len(sys.argv)}")
|
||||
for i, arg in enumerate(sys.argv):
|
||||
log.info(f" argv[{i}] = '{arg}'")
|
||||
log.info(f"=======================================")
|
||||
|
||||
# 2. Parseo manual de argumentos - compatible con -KEY=VALUE (WinDev) y /KEY VALUE (legacy)
|
||||
params = {}
|
||||
argv = sys.argv[1:]
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
# Formato -KEY=VALUE o /KEY=VALUE
|
||||
if '=' in arg:
|
||||
key, value = arg.split('=', 1)
|
||||
key = key.lstrip('-').lstrip('/')
|
||||
params[key] = value
|
||||
# Formato -KEY VALUE o /KEY VALUE (sin signo igual)
|
||||
elif arg.startswith('-') or arg.startswith('/'):
|
||||
key = arg.lstrip('-').lstrip('/')
|
||||
if i + 1 < len(argv) and not argv[i+1].startswith('-') and not argv[i+1].startswith('/'):
|
||||
params[key] = argv[i+1]
|
||||
i += 1
|
||||
else:
|
||||
params[key] = True
|
||||
i += 1
|
||||
|
||||
log.info(f"Parámetros interpretados: {list(params.keys())}")
|
||||
self._aplicar_dict_params(params)
|
||||
|
||||
# 3. Validación (Sin modo test por defecto)
|
||||
if not self.datos["servidor"]:
|
||||
log.error("FALTA PARÁMETRO: No se recibió DBServer. El proceso no puede continuar.")
|
||||
|
||||
# 4. Lógica de URL Dinámica (Solo SCAII o SCAF)
|
||||
self.datos["url_api"] = "https://104.192.7.152:3529/"
|
||||
|
||||
# 5. Valores por defecto finales
|
||||
if not self.datos["puerto"] or self.datos["puerto"] == 0:
|
||||
self.datos["puerto"] = 1433
|
||||
|
||||
# 6. Asegurar que las credenciales RPA estén en el dict de datos
|
||||
self.datos["usuario_rpa"] = self.usuario_rpa
|
||||
self.datos["password_rpa"] = self.password_rpa
|
||||
log.info(f"Configuración cargada. Servidor: {self.datos['servidor']}, DB: {self.datos['base_datos']}, Sistema: {self.datos['sistema']}")
|
||||
|
||||
def _leer_archivo_ini(self):
|
||||
config = configparser.ConfigParser()
|
||||
# Buscamos el archivo en el mismo directorio del EXE
|
||||
ruta_ini = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "SCAII_Sync.ini")
|
||||
try:
|
||||
if config.read(ruta_ini):
|
||||
# Buscar sección Database de forma insensible a mayúsculas
|
||||
section_db = next((s for s in config.sections() if s.lower() == "database"), None)
|
||||
if section_db:
|
||||
db = config[section_db]
|
||||
self.datos["servidor"] = db.get("Server", self.datos["servidor"])
|
||||
self.datos["base_datos"] = db.get("Database", self.datos["base_datos"])
|
||||
self.datos["usuario"] = db.get("User", self.datos["usuario"])
|
||||
self.datos["password"] = db.get("Pass", self.datos["password"])
|
||||
self.datos["puerto"] = int(db.get("Port", "1433"))
|
||||
log.info(f"Datos de DB cargados desde sección [{section_db}]")
|
||||
|
||||
# Buscar sección API de forma insensible a mayúsculas
|
||||
section_api = next((s for s in config.sections() if s.lower() == "api"), None)
|
||||
if section_api:
|
||||
api = config[section_api]
|
||||
self.usuario_rpa = api.get("User", self.usuario_rpa)
|
||||
self.password_rpa = api.get("Pass", self.password_rpa)
|
||||
log.info(f"Datos de API cargados desde sección [{section_api}]")
|
||||
|
||||
log.info(f"Archivo INI leído con éxito desde: {ruta_ini}")
|
||||
except Exception as e:
|
||||
log.error(f"Error al leer archivo INI: {e}")
|
||||
pass
|
||||
|
||||
def _aplicar_dict_params(self, params: dict):
|
||||
"""Aplica los parámetros desde un diccionario -KEY=VALUE (formato WinDev)"""
|
||||
if params.get("DBServer"): self.datos["servidor"] = params["DBServer"]
|
||||
if params.get("DBPort"): self.datos["puerto"] = int(params["DBPort"])
|
||||
if params.get("DBUser"): self.datos["usuario"] = params["DBUser"]
|
||||
if params.get("DBPass"): self.datos["password"] = params["DBPass"]
|
||||
if params.get("DBDatabase"): self.datos["base_datos"] = params["DBDatabase"]
|
||||
if params.get("DBSystem"): self.datos["sistema"] = params["DBSystem"]
|
||||
if params.get("DBPedi"): self.datos["pedimento"] = params["DBPedi"]
|
||||
if params.get("DBMoneda"): self.datos["tipo_moneda"] = params["DBMoneda"]
|
||||
if params.get("DBProcess") is not None: self.datos["proceso"] = int(params["DBProcess"])
|
||||
if params.get("RPAUser"): self.usuario_rpa = params["RPAUser"]
|
||||
if params.get("RPAPass"): self.password_rpa = params["RPAPass"]
|
||||
log.info(f"Parámetros aplicados -> Servidor: {self.datos['servidor']}, DB: {self.datos['base_datos']}, Sistema: {self.datos['sistema']}, Pedimento: {self.datos['pedimento']}")
|
||||
3
e.sh
Executable file
3
e.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
mv SCAII_Sincronizador_32bit.exe /mnt/c/Users/PC/Desktop
|
||||
474
gestor_base_datos.py
Normal file
474
gestor_base_datos.py
Normal file
@@ -0,0 +1,474 @@
|
||||
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()
|
||||
197
interfaz.py
Normal file
197
interfaz.py
Normal file
@@ -0,0 +1,197 @@
|
||||
import flet as ft
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
class InterfazFlet:
|
||||
def __init__(self, cliente_sinc=None):
|
||||
self.cliente = cliente_sinc
|
||||
self.page: Optional[ft.Page] = None
|
||||
|
||||
self.COLOR_TEXTO_TITULO = "#0066CC"
|
||||
self.COLOR_FRANJA_CELESTE = "#7CE0F6"
|
||||
|
||||
self.lista_resultados = ft.Column(
|
||||
expand=True, spacing=2, scroll=ft.ScrollMode.ALWAYS, auto_scroll=True
|
||||
)
|
||||
|
||||
self.cabecera_lista = ft.Container(
|
||||
content=ft.Row([
|
||||
ft.Text("Pedimento", width=150, color=ft.Colors.WHITE, weight="bold", size=12),
|
||||
ft.Text("Factura", width=120, color=ft.Colors.WHITE, weight="bold", size=12),
|
||||
ft.Text("Remesa", width=80, color=ft.Colors.WHITE, weight="bold", size=12),
|
||||
ft.Text("Estatus de Sincronización", expand=True, color=ft.Colors.WHITE, weight="bold", size=12),
|
||||
]),
|
||||
bgcolor=ft.Colors.BLUE_GREY_900,
|
||||
padding=ft.padding.only(left=20, top=10, right=20, bottom=10),
|
||||
border_radius=ft.border_radius.only(top_left=5, top_right=5)
|
||||
)
|
||||
|
||||
self.barra_progreso = ft.ProgressBar(value=0, color=ft.Colors.BLUE_600, bgcolor=ft.Colors.GREY_300)
|
||||
self.status_label = ft.Text("Listo", color=ft.Colors.BLACK, size=13, weight="w500")
|
||||
self.porcentaje_label = ft.Text("0%", color=ft.Colors.BLUE_700, weight="bold", size=14)
|
||||
|
||||
def main(self, page: ft.Page):
|
||||
self.page = page
|
||||
page.title = "Sistema de Control de Aduanas e Inventarios"
|
||||
page.theme_mode = ft.ThemeMode.LIGHT
|
||||
page.window_width = 1000
|
||||
page.window_height = 650
|
||||
page.padding = 20
|
||||
page.bgcolor = ft.Colors.GREY_50
|
||||
|
||||
titulo = ft.Text("Comunicación SCAII - WINSAAI.", size=24, color=self.COLOR_TEXTO_TITULO, weight="bold")
|
||||
|
||||
franjas_decorativas = ft.Column([
|
||||
ft.Container(height=3, bgcolor=ft.Colors.BLACK),
|
||||
ft.Container(height=15, bgcolor=self.COLOR_FRANJA_CELESTE)
|
||||
], spacing=0)
|
||||
|
||||
lista_container = ft.Container(
|
||||
content=ft.Column([self.cabecera_lista, self.lista_resultados], spacing=0),
|
||||
expand=True, bgcolor=ft.Colors.WHITE,
|
||||
border=ft.border.all(1, ft.Colors.GREY_300), border_radius=8
|
||||
)
|
||||
|
||||
footer = ft.Container(
|
||||
content=ft.Row([
|
||||
ft.Column([
|
||||
ft.Row([self.status_label, ft.Container(expand=True), self.porcentaje_label]),
|
||||
self.barra_progreso
|
||||
], expand=True, spacing=5)
|
||||
], alignment=ft.MainAxisAlignment.START, vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
||||
padding=15, bgcolor=ft.Colors.WHITE, border_radius=8,
|
||||
border=ft.border.all(1, ft.Colors.GREY_200),
|
||||
shadow=ft.BoxShadow(blur_radius=5, color=ft.Colors.GREY_200, offset=ft.Offset(0, 2))
|
||||
)
|
||||
|
||||
page.add(titulo, franjas_decorativas, lista_container,
|
||||
ft.Divider(height=10, color=ft.Colors.TRANSPARENT), footer)
|
||||
|
||||
if self.cliente:
|
||||
threading.Thread(target=self._hilo_sincronizacion, daemon=True).start()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
def agregar_fila(self, pedimento, factura, remesa, datos_completos, estatus):
|
||||
async def _do():
|
||||
# Panel de detalles (oculto inicialmente)
|
||||
campos_relevantes = {
|
||||
"NumFactura": "Factura",
|
||||
"Remesa": "Remesa",
|
||||
"IdProveedor": "ID Proveedor",
|
||||
"ValorDolares": "Valor Dólares",
|
||||
"PesoBruto": "Peso Bruto",
|
||||
"Transportista": "Transportista",
|
||||
"NumTrailer": "Caja/Trailer",
|
||||
"Incoterm": "Incoterm",
|
||||
"Consecutivo": "Folio Interno"
|
||||
}
|
||||
|
||||
filas_detalles = [
|
||||
ft.Row([
|
||||
ft.Text(f"{etiqueta}:", weight="bold", width=120, size=12, color=ft.Colors.GREY_700),
|
||||
ft.Text(str(datos_completos.get(clave, "N/A")), size=12, color=ft.Colors.BLUE_700),
|
||||
], spacing=10)
|
||||
for clave, etiqueta in campos_relevantes.items()
|
||||
]
|
||||
|
||||
panel_detalles = ft.Container(
|
||||
content=ft.Column(filas_detalles, spacing=6),
|
||||
visible=False, # <-- oculto por defecto
|
||||
bgcolor=ft.Colors.BLUE_50,
|
||||
padding=ft.padding.only(left=30, top=10, right=20, bottom=10),
|
||||
border=ft.border.only(
|
||||
bottom=ft.BorderSide(1, ft.Colors.BLUE_100),
|
||||
left=ft.BorderSide(3, ft.Colors.BLUE_400), # acento visual izquierdo
|
||||
),
|
||||
)
|
||||
|
||||
icono_expand = ft.Icon(ft.Icons.KEYBOARD_ARROW_DOWN, size=16, color=ft.Colors.GREY_400)
|
||||
|
||||
def toggle_detalles(e, p=panel_detalles, i=icono_expand):
|
||||
p.visible = not p.visible
|
||||
i.name = ft.Icons.KEYBOARD_ARROW_UP if p.visible else ft.Icons.KEYBOARD_ARROW_DOWN
|
||||
self.page.update()
|
||||
|
||||
fila_header = ft.Container(
|
||||
content=ft.Row([
|
||||
ft.Text(pedimento, width=150, size=12, color=ft.Colors.BLACK),
|
||||
ft.Text(factura, width=120, size=12, color=ft.Colors.BLACK),
|
||||
ft.Text(remesa, width=80, size=12, color=ft.Colors.BLACK),
|
||||
ft.Text(estatus, expand=True, size=12, color=ft.Colors.BLUE_700, weight="bold"),
|
||||
icono_expand,
|
||||
]),
|
||||
padding=ft.padding.symmetric(horizontal=20, vertical=8),
|
||||
border=ft.border.only(bottom=ft.BorderSide(1, ft.Colors.GREY_100)),
|
||||
on_click=toggle_detalles,
|
||||
ink=True,
|
||||
tooltip="Clic para ver detalles"
|
||||
)
|
||||
|
||||
# Agrupamos header + panel en una Column
|
||||
fila_completa = ft.Column([fila_header, panel_detalles], spacing=0)
|
||||
self.lista_resultados.controls.append(fila_completa)
|
||||
self.page.update()
|
||||
|
||||
if self.page:
|
||||
self.page.run_task(_do)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
def actualizar_estado_fila(self, indice, estatus):
|
||||
async def _do():
|
||||
if indice < len(self.lista_resultados.controls):
|
||||
# controls[indice] = Column → controls[0] = header Container → Row → controls[3] = Text estatus
|
||||
fila_header = self.lista_resultados.controls[indice].controls[0]
|
||||
fila_header.content.controls[3].value = estatus
|
||||
self.page.update()
|
||||
if self.page:
|
||||
self.page.run_task(_do)
|
||||
|
||||
|
||||
def actualizar_progreso(self, valor):
|
||||
async def _do():
|
||||
self.barra_progreso.value = valor
|
||||
self.porcentaje_label.value = f"{int(valor * 100)}%"
|
||||
self.page.update()
|
||||
if self.page:
|
||||
self.page.run_task(_do)
|
||||
|
||||
def actualizar_estado_texto(self, texto):
|
||||
async def _do():
|
||||
self.status_label.value = texto
|
||||
self.page.update()
|
||||
if self.page:
|
||||
self.page.run_task(_do)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
def _hilo_sincronizacion(self):
|
||||
try:
|
||||
if self.cliente:
|
||||
self.cliente.proceso_principal()
|
||||
|
||||
async def _done():
|
||||
self.status_label.value = "Listo"
|
||||
self.barra_progreso.value = 1.0
|
||||
self.porcentaje_label.value = "100%"
|
||||
self.page.update()
|
||||
if self.page:
|
||||
self.page.run_task(_done)
|
||||
|
||||
except Exception as ex:
|
||||
print(f"[UI ERROR] {ex}")
|
||||
async def _err():
|
||||
self.status_label.value = f"Error: {ex}"
|
||||
self.page.update()
|
||||
if self.page:
|
||||
self.page.run_task(_err)
|
||||
|
||||
|
||||
def lanzar_interfaz(cliente=None):
|
||||
app = InterfazFlet(cliente)
|
||||
if cliente:
|
||||
cliente.gui = app
|
||||
ft.app(target=app.main)
|
||||
return app
|
||||
|
||||
if __name__ == "__main__":
|
||||
ft.app(target=InterfazFlet().main)
|
||||
38
logger_utils.py
Normal file
38
logger_utils.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
def setup_logger():
|
||||
# Obtener la ruta del ejecutable o del script
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Si es un ejecutable generado por PyInstaller
|
||||
application_path = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# Si es el script de Python normal
|
||||
application_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
log_file = os.path.join(application_path, "debug_sync.log")
|
||||
|
||||
# Crear el logger
|
||||
logger = logging.getLogger("SCAII_Sync")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Evitar duplicados si se llama varias veces
|
||||
if not logger.handlers:
|
||||
# Formato: [2024-05-06 10:00:00] [INFO] Mensaje
|
||||
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Handler para archivo (modo 'a' para anexar, 'w' para sobrescribir en cada inicio)
|
||||
file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Handler para consola
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
return logger
|
||||
|
||||
# Instancia global para importar en otros archivos
|
||||
log = setup_logger()
|
||||
59
main.py
Executable file
59
main.py
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from config import ConfiguracionApp
|
||||
from gestor_base_datos import GestorBaseDatos
|
||||
from motor_sincronizacion import MotorSincronizacion
|
||||
from cliente_api import ClienteAPI
|
||||
from interfaz import lanzar_interfaz
|
||||
from logger_utils import log
|
||||
|
||||
class ClienteSincronizacionSCAII:
|
||||
def __init__(self):
|
||||
self.config = ConfiguracionApp()
|
||||
self.config.cargar()
|
||||
|
||||
self.db = GestorBaseDatos(self.config.datos)
|
||||
|
||||
# Cliente API con sistema correcto para Login
|
||||
self.api = ClienteAPI(self.config.datos)
|
||||
|
||||
# Motor vinculado al API
|
||||
self.motor = MotorSincronizacion(self.db, self.api)
|
||||
self.gui = None
|
||||
|
||||
def iniciar(self):
|
||||
# 1. Conexión a la DB
|
||||
if not self.db.conectar():
|
||||
log.error("No se pudo conectar a la base de datos al iniciar.")
|
||||
return
|
||||
|
||||
# 2. Lanzar la Interfaz (Flet)
|
||||
log.info("Lanzando Interfaz gráfica (Flet)...")
|
||||
self.gui = lanzar_interfaz(self)
|
||||
|
||||
def proceso_principal(self):
|
||||
"""Orquestación delegada al Motor"""
|
||||
# Vincular callbacks a la UI
|
||||
if self.gui:
|
||||
self.motor.callback_progreso = self.gui.actualizar_progreso
|
||||
self.motor.callback_fila = self.gui.agregar_fila
|
||||
self.motor.callback_actualizar_fila = self.gui.actualizar_estado_fila
|
||||
self.motor.callback_estado_texto = self.gui.actualizar_estado_texto
|
||||
|
||||
# Ejecutar la lógica robusta del motor (que ya tiene el debug)
|
||||
exito = self.motor.proceso_principal()
|
||||
|
||||
if exito:
|
||||
if self.gui:
|
||||
self.gui.status_label.value = "¡Sincronización Exitosa!"
|
||||
self.gui.page.update()
|
||||
else:
|
||||
log.error("El proceso de sincronización falló.")
|
||||
if self.gui:
|
||||
self.gui.status_label.value = "Error en Proceso (Ver debug_sync.log)"
|
||||
self.gui.page.update()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = ClienteSincronizacionSCAII()
|
||||
app.iniciar()
|
||||
218
motor_sincronizacion.py
Normal file
218
motor_sincronizacion.py
Normal 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)
|
||||
61
registros/scaf/reg_501.py
Normal file
61
registros/scaf/reg_501.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador501SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(datos_pedi: Dict[str, Any], totales: Dict[str, float],
|
||||
datos_cliente: Dict[str, Any], total_trailers: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica específica para SCAF (Registro 501).
|
||||
"""
|
||||
|
||||
# Lógica de Destino/Zona (SCAF a veces usa códigos distintos, pero el if parece igual)
|
||||
destino = datos_pedi.get("OPCIONDESTINO", "")
|
||||
codigo_destino = "9" if "Interior" in destino else "7" if any(x in destino for x in ["Región", "Franja"]) else ""
|
||||
|
||||
clave_transfer = datos_cliente.get("CLAVETRANSFER", "").strip() if datos_cliente else ""
|
||||
|
||||
# Construcción del Registro 501 SCAF
|
||||
registro = {
|
||||
"tipo_operacion_501": datos_pedi.get("Tipo"),
|
||||
"clave_documento_501": datos_pedi.get("Clave"),
|
||||
"numero_pedimento_501": (datos_pedi.get("Pedimento") or "")[8:15],
|
||||
"codigo_importador_exportador_501": clave_transfer if clave_transfer else " ",
|
||||
"monto_fletes_501": totales["flete"],
|
||||
"monto_seguros_501": totales["seguros"],
|
||||
"costo_embalajes_501": totales["embalajes"],
|
||||
"cargos_incrementables_501": totales["otros_incrementa"],
|
||||
"cargos_deducibles_501": 0,
|
||||
"peso_total_bruto_501": totales["peso_bruto"],
|
||||
"total_bultos_501": totales["bultos"],
|
||||
"metodo_transporte_entrada_501": datos_pedi.get("TransE"),
|
||||
"metodo_transporte_arribo_501": datos_pedi.get("TransA"),
|
||||
"metodo_transporte_salida_501": datos_pedi.get("TransS"),
|
||||
"codigo_destino_zona_501": codigo_destino,
|
||||
"numero_referencia_501": "",
|
||||
"moneda_costos_incrementables_501": "ME",
|
||||
"codigo_aduana_despacho_501": datos_pedi.get("Aduana"),
|
||||
"numero_patente_501": (datos_pedi.get("Pedimento") or "")[3:7],
|
||||
"origenPedimento": 1,
|
||||
"total_vehiculos_501": total_trailers,
|
||||
"rfc_importador_exportador_501": " ",
|
||||
"curp_importador_exportador_501": " ",
|
||||
"nombre_completo_importador_exportador_501": " ",
|
||||
"calle_direccion_importador_exportador_501": " ",
|
||||
"numero_exterior_direccion_importador_exportador_501": " ",
|
||||
"numero_interior_direccion_importador_exportador_501": " ",
|
||||
"colonia_direccion_importador_exportador_501": " ",
|
||||
"localidad_direccion_importador_exportador_501": " ",
|
||||
"municipio_direccion_importador_exportador_501": " ",
|
||||
"estado_direccion_importador_exportador_501": " ",
|
||||
"pais_direccion_importador_exportador_501": " ",
|
||||
"codigo_postal_direccion_importador_exportador_501": " ",
|
||||
"monto_fletes_decrementables_501": 0,
|
||||
"monto_seguros_decrementables_501": 0,
|
||||
"monto_carga_decrementables_501": 0,
|
||||
"monto_descarga_decrementables_501": 0,
|
||||
"monto_otros_decrementables_501": 0,
|
||||
"MarcaBulto1_501": "S/M", # SCAF usa S/M mayormente
|
||||
"MarcaBulto2_501": "S/N"
|
||||
}
|
||||
|
||||
return registro
|
||||
18
registros/scaf/reg_502.py
Normal file
18
registros/scaf/reg_502.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador502SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(id_pedimento: int, datos_transporte: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 502 (Transporte) - SCAF.
|
||||
"""
|
||||
placas = datos_transporte.get("NUMEROPLACAS", " ") if datos_transporte else " "
|
||||
pais = datos_transporte.get("PAIS", " ") if datos_transporte else " "
|
||||
|
||||
registro = {
|
||||
"id_pedimento": id_pedimento,
|
||||
"numero_placas_502": placas,
|
||||
"codigo_pais_transporte_502": pais
|
||||
}
|
||||
|
||||
return registro
|
||||
21
registros/scaf/reg_503.py
Normal file
21
registros/scaf/reg_503.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador503SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(id_pedimento: int, factura_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 503 (Guías) - SCAF.
|
||||
"""
|
||||
|
||||
num_niu = factura_data.get("NumNIU", " ")
|
||||
tipo_guia = factura_data.get("TipoGuia", " ")
|
||||
total_guias = factura_data.get("CantGuias", 0)
|
||||
|
||||
registro = {
|
||||
"id_pedimento": id_pedimento,
|
||||
"numero_guia_manifiesto_embarque_503": num_niu if num_niu else " ",
|
||||
"identificador_guia_503": tipo_guia if tipo_guia else " ",
|
||||
"total_guias_503": total_guias if total_guias else 0
|
||||
}
|
||||
|
||||
return registro
|
||||
33
registros/scaf/reg_504.py
Normal file
33
registros/scaf/reg_504.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class Generador504SCAF:
|
||||
@staticmethod
|
||||
def preparar_listado_json(id_pedimento: int, contenedores_raw: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Procesa el string de contenedores (CONT1|TIPO1,CONT2|TIPO2)
|
||||
y genera una lista de registros para la API - Versión SCAF.
|
||||
"""
|
||||
registros = []
|
||||
|
||||
if not contenedores_raw or contenedores_raw.strip() == "":
|
||||
return registros
|
||||
|
||||
bloques = contenedores_raw.split(",")
|
||||
|
||||
for bloque in bloques:
|
||||
if not bloque.strip():
|
||||
continue
|
||||
|
||||
partes = bloque.split("|")
|
||||
num_contenedor = partes[0].strip() if len(partes) > 0 else ""
|
||||
tipo_contenedor = partes[1].strip() if len(partes) > 1 else " "
|
||||
|
||||
if num_contenedor:
|
||||
registros.append({
|
||||
"id_pedimento_501": id_pedimento,
|
||||
"numero_contenedor_504": num_contenedor,
|
||||
"tipo_contenedor_504": tipo_contenedor,
|
||||
"status_504": "1"
|
||||
})
|
||||
|
||||
return registros
|
||||
104
registros/scaf/reg_505.py
Normal file
104
registros/scaf/reg_505.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class Generador505SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(fac: Dict[str, Any], id_pedimento: int, datos_empresa: Dict[str, Any],
|
||||
datos_prov: Dict[str, Any], datos_trans: Optional[Dict[str, Any]],
|
||||
tipo_moneda_global: str, tc_global: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 505 (Facturas) - SCAF.
|
||||
"""
|
||||
|
||||
clave_moneda = fac.get("ClaveMoneda", "")
|
||||
valor_mc = fac.get("ValorImpoMC", 0)
|
||||
valor_me = fac.get("ValorImpoME", 0)
|
||||
|
||||
if tipo_moneda_global == "ME":
|
||||
clave_moneda = "USD"
|
||||
valor_mc = valor_me
|
||||
elif tipo_moneda_global == "MN":
|
||||
clave_moneda = "MXP"
|
||||
valor_mc = fac.get("ValorImpoMN", 0)
|
||||
|
||||
# SCAF usa el mismo mapeo de Valor Aduana que SCAII
|
||||
met_val_raw = fac.get("MetValor", 0)
|
||||
met_val_map = {1: "VTM", 2: "VMI", 3: "VMS", 4: "VPU", 5: "VR", 6: "A78"}
|
||||
met_val_str = f"VALADU.{met_val_map.get(met_val_raw, met_val_raw)}"
|
||||
|
||||
# 3. Incoterm
|
||||
incoterm = fac.get("Incoterm", "")
|
||||
incoterm_full = incoterm if incoterm else ""
|
||||
|
||||
# 4. Otros mapeos
|
||||
obs = fac.get("ObservacionesVU", "").replace("\r", "").replace("\n", "").replace("\t", "")
|
||||
contenedores = fac.get("ContenedoresTipo", "").replace(",", "|")
|
||||
|
||||
registro = {
|
||||
"numero_factura_505": fac.get("NumFactura"),
|
||||
"fecha_facturacion_505": str(fac.get("FechaFactura"))[:10],
|
||||
"termino_facturacion_505": incoterm if incoterm else "",
|
||||
"moneda_facturacion_505": clave_moneda if clave_moneda else "",
|
||||
"valor_moneda_facturacion_505": str(float(valor_mc or 0)),
|
||||
"valor_dolares_505": str(float(valor_me or 0)),
|
||||
"codigo_proveedor_comprador_505": datos_prov.get("CLAVETRANSFER", ""),
|
||||
"e_document_505": fac.get("EDocument", ""),
|
||||
"serie_certificado_cove_505": fac.get("NumeroCertificado", ""),
|
||||
"firma_electronica_cove_505": fac.get("FirmaElectronica", ""),
|
||||
"observaciones_factura_505": obs if obs else "",
|
||||
"numero_economico_505": contenedores if contenedores else "",
|
||||
"tipo_contenedor_numero_economico_505": str(fac.get("Remesa") or ""),
|
||||
"numero_contenedor_505": contenedores if contenedores else "",
|
||||
"tipo_contenedor_505": str(fac.get("Remesa") or ""),
|
||||
"numero_remesa_505": str(fac.get("Remesa") or ""),
|
||||
"tipo_operacion_505": "",
|
||||
"certificado_origen_505": "S" if fac.get("FUNGECOMOCO") else "N",
|
||||
"numero_certificado_origen_505": "",
|
||||
"numero_exportador_confiable_505": datos_empresa.get("NUMDEEXPORTADORCONFIABLE") or "",
|
||||
"incoterm_505": incoterm_full,
|
||||
"metodo_valoracion_505": met_val_str,
|
||||
"vinculacion_505": str(datos_prov.get("VINCULACION", 0)),
|
||||
"subdivision_505": "S" if fac.get("SUBDIVISION") else "N",
|
||||
"tipo_cambio_usd_505": str(float(fac.get("TipoCambio") or 0)),
|
||||
"se_genero_en_contingencia_505": "S" if fac.get("MODOCONTINGENCIA") else "N",
|
||||
"operacion_cove_505": fac.get("NUMOPERACIONVU", ""),
|
||||
"transportista_clave_505": fac.get("TRANSPORTISTA", ""),
|
||||
"rfc_505": datos_trans.get("RFC", "") if datos_trans else "",
|
||||
"caat_505": datos_trans.get("CODIGOCAAT", "") if datos_trans else "",
|
||||
"id_pedimento": id_pedimento,
|
||||
"PaisFactura_505": datos_prov.get("PAIS", ""),
|
||||
"Bultos_cantidad_505": str(fac.get("CANTBULTOS", 0)),
|
||||
"provcomp_numregidtrib_505": " ",
|
||||
"provcomp_rfc_505": " ",
|
||||
"provcomp_curp_505": " ",
|
||||
"provcomp_nombre_505": " ",
|
||||
"provcomp_calle_505": " ",
|
||||
"provcomp_numero_exterior_505": " ",
|
||||
"provcomp_numero_interior_505": " ",
|
||||
"provcomp_colonia_505": " ",
|
||||
"provcomp_localidad_505": " ",
|
||||
"provcomp_municipio_505": " ",
|
||||
"provcomp_estado_505": " ",
|
||||
"provcomp_pais_505": " ",
|
||||
"provcomp_codigo_postal_505": " ",
|
||||
"destinatario_clave_505": " ",
|
||||
"destinatario_numregidtrib_505": " ",
|
||||
"destinatario_rfc_505": " ",
|
||||
"destinatario_curp_505": " ",
|
||||
"destinatario_nombre_505": " ",
|
||||
"destinatario_calle_505": " ",
|
||||
"destinatario_numero_exterior_505": " ",
|
||||
"destinatario_numero_interior_505": " ",
|
||||
"destinatario_colonia_505": " ",
|
||||
"destinatario_localidad_505": " ",
|
||||
"destinatario_referencia_505": " ",
|
||||
"destinatario_municipio_505": " ",
|
||||
"destinatario_estado_505": " ",
|
||||
"destinatario_pais_505": " ",
|
||||
"destinatario_codigo_postal_505": " ",
|
||||
"uuid_505": " ",
|
||||
"path_pdf_cfdi_505": " ",
|
||||
"path_xml_cfdi_505": " ",
|
||||
"estatus_cove_505": " "
|
||||
}
|
||||
|
||||
return registro
|
||||
101
registros/scaf/reg_551.py
Normal file
101
registros/scaf/reg_551.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class Generador551SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(par: Dict[str, Any], id_factura: int, datos_parte: Dict[str, Any],
|
||||
um_data: Optional[Dict[str, Any]], met_val: str, cla_mon: str,
|
||||
num_remesa: int, num_partida_remesa: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 551 (Partidas) - SCAF.
|
||||
Homologado con la lógica de pesos y valores de WinDev.
|
||||
"""
|
||||
|
||||
# 1. Lógica de Fracción (Regla Octava)
|
||||
frac_raw = par.get("Fraccion", "")
|
||||
frac_oct = par.get("FraccionOctava", "")
|
||||
frac_final = frac_oct if frac_oct and frac_oct.strip() else frac_raw
|
||||
|
||||
# 2. Descripciones
|
||||
desc_esp = par.get("Descripcion", " ").replace("\r", "").replace("\n", "").replace("\t", "")
|
||||
|
||||
# 3. Arancel (Mapping)
|
||||
tipo_frac = par.get("TipoFracc", "")
|
||||
arancel_map = {"TLCS": 3, "GENERAL": 0, "PROSEC": 1}
|
||||
id_arancel = str(arancel_map.get(tipo_frac, 0))
|
||||
|
||||
# 4. Unidad de Medida OMA y Comercial
|
||||
oma_final = str(par.get("ClaveOMA") or " ").strip()
|
||||
oma_raw = str(par.get("UnidadMedida", "")).strip().upper()
|
||||
if not oma_final or oma_final == "None":
|
||||
oma_final = "C62_1" if oma_raw in ["6", "PZA"] else oma_raw
|
||||
|
||||
map_um_num = {
|
||||
"PZA": "6", "PIEZA": "6", "PZ": "6", "PC": "6", "PIEZAS": "6",
|
||||
"KGS": "1", "KGM": "1", "KG": "1", "KILOS": "1", "KILO": "1",
|
||||
"LTS": "5", "L": "5", "LITROS": "5", "LITRO": "5",
|
||||
"MTR": "2", "M": "2", "METROS": "2", "METRO": "2",
|
||||
"PAR": "7", "PARES": "7",
|
||||
"CJ": "3", "CAJA": "3", "CAJAS": "3",
|
||||
"JGO": "12", "JUEGO": "12", "SET": "12", "KIT": "12"
|
||||
}
|
||||
|
||||
# Priorizar la clave que venga de la base de datos (CLAVEOMA)
|
||||
if um_data and um_data.get("CLAVEOMA"):
|
||||
um_comercial_num = str(um_data["CLAVEOMA"]).strip()
|
||||
else:
|
||||
um_comercial_num = map_um_num.get(oma_raw, oma_raw)
|
||||
|
||||
registro = {
|
||||
"id_factura": id_factura,
|
||||
"fraccion_551": str(frac_final),
|
||||
"descripcion_mercancia_551": desc_esp[:250],
|
||||
"numero_parte_551": str(par.get("NumParte") or " "),
|
||||
"valor_mercancia_551": float(par.get("ValorDolares") or 0), # SCAF suele usar Dolares para MC
|
||||
"cantidad_comercial_551": float(par.get("Cantidad") or 0),
|
||||
"unidad_medida_comercial_551": um_comercial_num,
|
||||
"cantidad_tarifa_551": float(par.get("Cantidad") or 0),
|
||||
"umt_551": " ",
|
||||
"valor_agregado_551": "0",
|
||||
"vinculacion_551": "0",
|
||||
"metodo_valoracion_551": str(met_val),
|
||||
"marca_551": str(par.get("Marca") or " ").strip() or " ",
|
||||
"modelo_551": str(par.get("Modelo") or " ").strip() or " ",
|
||||
"pais_origen_destino_551": str(par.get("PaisOrigen") or " "),
|
||||
"pais_comprador_vendedor_551": "MEX",
|
||||
"entidad_federativa_origen_551": " ",
|
||||
"entidad_federativa_destino_551": " ",
|
||||
"entidad_federativa_comprador_551": " ",
|
||||
"entidad_federativa_vendedor_551": " ",
|
||||
"identificador_arancel_aplicar_551": id_arancel,
|
||||
"clave_industria_551": str(par.get("Sector") or " "),
|
||||
"peso_bruto_551": float(par.get("PesoBruto") or 0),
|
||||
"uso_mercancia_551": " ",
|
||||
"estado_mercancia_551": " ",
|
||||
"moneda_partida_551": str(cla_mon),
|
||||
"numero_factura_551": str(par.get("NumFactura") or " "),
|
||||
"tipo_bulto_551": " ",
|
||||
"cantidad_bultos_551": float(par.get("CantBultos") or 0),
|
||||
|
||||
# --- PARIDAD WINDEV ---
|
||||
"peso_bruto_kg_551": str(float(par.get("PesoNeto") or 0)),
|
||||
"peso_neto_kg_551": str(float(par.get("ValorAduanasMN") or 0)),
|
||||
|
||||
"numero_remesa_551": str(num_remesa or " "),
|
||||
"numero_partida_remesa_551": str(num_partida_remesa),
|
||||
"cantidad_alterna_551": float(par.get("CantidadAlterna") or 0),
|
||||
"unidad_medida_alterna_551": str(par.get("UnidadAlterna", " ")),
|
||||
"descripcion_ingles_551": " ",
|
||||
"identificador_sistema_scaii_scaf_551": "SCAF",
|
||||
"vu_submodelo_551": " ",
|
||||
"vu_serie_551": " ",
|
||||
"valor_unitario_aduana_551": str(float(par.get("CostoUnitarioME") or 0)),
|
||||
"valor_dolares_551": str(float(par.get("ValorDolares") or 0)),
|
||||
"unidad_medida_oma_551": oma_final,
|
||||
"valor_dolares_cove_551": "0",
|
||||
"subdivision_fraccion_551": str(frac_final)[-2:] if len(str(frac_final)) >= 2 else "00",
|
||||
"codigo_carta_porte_551": " ",
|
||||
"nodo_551": "85858585",
|
||||
"CantidadComercialOMA_551": str(float(par.get("Cantidad") or 0))
|
||||
}
|
||||
|
||||
return registro
|
||||
16
registros/scaf/reg_554.py
Normal file
16
registros/scaf/reg_554.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador554SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(iden: Dict[str, Any], id_partida: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 554 (Identificadores de Partidas) - SCAF.
|
||||
"""
|
||||
registro = {
|
||||
"tipo_caso_554": iden.get("identificador", " "),
|
||||
"complemento_caso_1_554": iden.get("complemento_1", " "),
|
||||
"complemento_caso_2_554": iden.get("complemento_2", " "),
|
||||
"complemento_caso_3_554": iden.get("complemento_3", " "),
|
||||
"id_partida": id_partida
|
||||
}
|
||||
return registro
|
||||
17
registros/scaf/reg_558.py
Normal file
17
registros/scaf/reg_558.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador558SCAF:
|
||||
@staticmethod
|
||||
def preparar_json(id_partida: int, observaciones: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 558 (Observaciones de Partidas) - SCAF.
|
||||
"""
|
||||
if not observaciones:
|
||||
observaciones = " "
|
||||
|
||||
obs_limpia = observaciones.replace("\r", "").replace("\n", "").replace("\t", "").strip()
|
||||
|
||||
return {
|
||||
"observaciones_558": obs_limpia[:1000],
|
||||
"id_partida": id_partida
|
||||
}
|
||||
62
registros/scaii/reg_501.py
Normal file
62
registros/scaii/reg_501.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador501:
|
||||
@staticmethod
|
||||
def preparar_json(datos_pedi: Dict[str, Any], totales: Dict[str, float],
|
||||
datos_cliente: Dict[str, Any], total_trailers: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Unifica la lógica de Get_Regs501, _Def y _Expo en una sola estructura.
|
||||
"""
|
||||
|
||||
# Lógica de Destino/Zona (Opcional)
|
||||
destino = datos_pedi.get("Destino", "")
|
||||
codigo_destino = "9" if "Interior" in destino else "7" if any(x in destino for x in ["Región", "Franja"]) else ""
|
||||
|
||||
# Clave del Importador/Exportador
|
||||
clave_transfer = datos_cliente.get("CLAVETRANSFER", "").strip() if datos_cliente else ""
|
||||
|
||||
# Construcción del Registro 501
|
||||
registro = {
|
||||
"tipo_operacion_501": datos_pedi.get("Tipo") or "",
|
||||
"clave_documento_501": datos_pedi.get("Clave") or "",
|
||||
"numero_pedimento_501": (datos_pedi.get("Pedimento") or "")[8:15],
|
||||
"codigo_importador_exportador_501": clave_transfer if clave_transfer else " ",
|
||||
"monto_fletes_501": totales["flete"],
|
||||
"monto_seguros_501": totales["seguros"],
|
||||
"costo_embalajes_501": totales["embalajes"],
|
||||
"cargos_incrementables_501": totales["otros_incrementa"],
|
||||
"cargos_deducibles_501": 0,
|
||||
"peso_total_bruto_501": totales["peso_bruto"],
|
||||
"total_bultos_501": totales["bultos"],
|
||||
"metodo_transporte_entrada_501": str(datos_pedi.get("TransE") or ""),
|
||||
"metodo_transporte_arribo_501": str(datos_pedi.get("TransA") or ""),
|
||||
"metodo_transporte_salida_501": str(datos_pedi.get("TransS") or ""),
|
||||
"codigo_destino_zona_501": codigo_destino,
|
||||
"numero_referencia_501": "",
|
||||
"moneda_costos_incrementables_501": "ME", # O tomar de la primera factura
|
||||
"codigo_aduana_despacho_501": str(datos_pedi.get("Aduana") or ""),
|
||||
"numero_patente_501": (datos_pedi.get("Pedimento") or "")[3:7],
|
||||
"origenPedimento": 1,
|
||||
"total_vehiculos_501": total_trailers,
|
||||
"rfc_importador_exportador_501": " ",
|
||||
"curp_importador_exportador_501": " ",
|
||||
"nombre_completo_importador_exportador_501": " ",
|
||||
"calle_direccion_importador_exportador_501": " ",
|
||||
"numero_exterior_direccion_importador_exportador_501": " ",
|
||||
"numero_interior_direccion_importador_exportador_501": " ",
|
||||
"colonia_direccion_importador_exportador_501": " ",
|
||||
"localidad_direccion_importador_exportador_501": " ",
|
||||
"municipio_direccion_importador_exportador_501": " ",
|
||||
"estado_direccion_importador_exportador_501": " ",
|
||||
"pais_direccion_importador_exportador_501": " ",
|
||||
"codigo_postal_direccion_importador_exportador_501": " ",
|
||||
"monto_fletes_decrementables_501": 0,
|
||||
"monto_seguros_decrementables_501": 0,
|
||||
"monto_carga_decrementables_501": 0,
|
||||
"monto_descarga_decrementables_501": 0,
|
||||
"monto_otros_decrementables_501": 0,
|
||||
"MarcaBulto1_501": "S/N",
|
||||
"MarcaBulto2_501": "S/N"
|
||||
}
|
||||
|
||||
return registro
|
||||
20
registros/scaii/reg_502.py
Normal file
20
registros/scaii/reg_502.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador502SCAII:
|
||||
@staticmethod
|
||||
def preparar_json(id_pedimento: int, datos_transporte: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 502 (Transporte) - SCAII.
|
||||
"""
|
||||
|
||||
# En WinDev: GTrailers.NUMEROPLACAS si existe, si no " "
|
||||
placas = datos_transporte.get("NUMEROPLACAS", " ") if datos_transporte else " "
|
||||
pais = datos_transporte.get("PAIS", " ") if datos_transporte else " "
|
||||
|
||||
registro = {
|
||||
"id_pedimento": id_pedimento,
|
||||
"numero_placas_502": placas,
|
||||
"codigo_pais_transporte_502": pais
|
||||
}
|
||||
|
||||
return registro
|
||||
21
registros/scaii/reg_503.py
Normal file
21
registros/scaii/reg_503.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador503SCAII:
|
||||
@staticmethod
|
||||
def preparar_json(id_pedimento: int, factura_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 503 (Guías) - SCAII.
|
||||
"""
|
||||
|
||||
num_niu = factura_data.get("num_niu", " ")
|
||||
tipo_guia = factura_data.get("tipo_guia", " ")
|
||||
total_guias = factura_data.get("total_guias", 0)
|
||||
|
||||
registro = {
|
||||
"id_pedimento": id_pedimento,
|
||||
"numero_guia_manifiesto_embarque_503": num_niu if num_niu else " ",
|
||||
"identificador_guia_503": tipo_guia if tipo_guia else " ",
|
||||
"total_guias_503": total_guias if total_guias else 0
|
||||
}
|
||||
|
||||
return registro
|
||||
35
registros/scaii/reg_504.py
Normal file
35
registros/scaii/reg_504.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class Generador504SCAII:
|
||||
@staticmethod
|
||||
def preparar_listado_json(id_pedimento: int, contenedores_raw: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Procesa el string de contenedores (CONT1|TIPO1,CONT2|TIPO2)
|
||||
y genera una lista de registros para la API.
|
||||
"""
|
||||
registros = []
|
||||
|
||||
if not contenedores_raw or contenedores_raw.strip() == "":
|
||||
return registros
|
||||
|
||||
# En WinDev: FOR EACH STRING sContenedores OF DS_Facturas.c1 SEPARATED BY ","
|
||||
bloques = contenedores_raw.split(",")
|
||||
|
||||
for bloque in bloques:
|
||||
if not bloque.strip():
|
||||
continue
|
||||
|
||||
# En WinDev: (sNumeroContenedor,sTipoContenedor) = StringSplit(sContenedores,"|")
|
||||
partes = bloque.split("|")
|
||||
num_contenedor = partes[0].strip() if len(partes) > 0 else ""
|
||||
tipo_contenedor = partes[1].strip() if len(partes) > 1 else " "
|
||||
|
||||
if num_contenedor:
|
||||
registros.append({
|
||||
"id_pedimento_501": id_pedimento,
|
||||
"numero_contenedor_504": num_contenedor,
|
||||
"tipo_contenedor_504": tipo_contenedor,
|
||||
"status_504": "1"
|
||||
})
|
||||
|
||||
return registros
|
||||
106
registros/scaii/reg_505.py
Normal file
106
registros/scaii/reg_505.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class Generador505SCAII:
|
||||
@staticmethod
|
||||
def preparar_json(fac: Dict[str, Any], id_pedimento: int, datos_empresa: Dict[str, Any],
|
||||
datos_prov: Dict[str, Any], datos_trans: Optional[Dict[str, Any]],
|
||||
tipo_moneda_global: str, tc_global: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 505 (Facturas) - SCAII.
|
||||
"""
|
||||
|
||||
# 1. Lógica de Moneda y Valores
|
||||
clave_moneda = fac.get("ClaveMoneda", "")
|
||||
valor_mc = fac.get("ValorMC", 0)
|
||||
valor_me = fac.get("ValorImpoME", 0)
|
||||
|
||||
if tipo_moneda_global == "ME":
|
||||
clave_moneda = "USD"
|
||||
valor_mc = valor_me
|
||||
elif tipo_moneda_global == "MN":
|
||||
clave_moneda = "MXP"
|
||||
# Asumiendo lógica estándar si no es FP
|
||||
valor_mc = fac.get("ValorImpoMN", 0)
|
||||
|
||||
# 2. Lógica de Método de Valoración (Mapping)
|
||||
met_val_raw = fac.get("MetodoValoracion", 0)
|
||||
met_val_map = {1: "VTM", 2: "VMI", 3: "VMS", 4: "VPU", 5: "VR", 6: "A78"}
|
||||
met_val_str = f"VALADU.{met_val_map.get(met_val_raw, met_val_raw)}"
|
||||
|
||||
# 3. Incoterm
|
||||
incoterm = fac.get("Incoterm", "")
|
||||
incoterm_full = incoterm if incoterm else ""
|
||||
|
||||
# 4. Otros mapeos
|
||||
obs = fac.get("Observaciones", "").replace("\r", "").replace("\n", "").replace("\t", "")
|
||||
contenedores = fac.get("ContenedoresTipo", "").replace(",", "|")
|
||||
|
||||
registro = {
|
||||
"numero_factura_505": fac.get("NumFactura"),
|
||||
"fecha_facturacion_505": str(fac.get("Fecha"))[:10], # YYYY-MM-DD
|
||||
"termino_facturacion_505": incoterm if incoterm else "",
|
||||
"moneda_facturacion_505": clave_moneda if clave_moneda else "",
|
||||
"valor_moneda_facturacion_505": str(float(valor_mc or 0)),
|
||||
"valor_dolares_505": str(float(valor_me or 0)),
|
||||
"codigo_proveedor_comprador_505": datos_prov.get("CLAVETRANSFER", ""),
|
||||
"e_document_505": fac.get("EDocument", ""),
|
||||
"serie_certificado_cove_505": fac.get("NumCertificado", ""),
|
||||
"firma_electronica_cove_505": fac.get("FirmaElectronica", ""),
|
||||
"observaciones_factura_505": obs if obs else "",
|
||||
"numero_economico_505": contenedores if contenedores else "",
|
||||
"tipo_contenedor_numero_economico_505": str(fac.get("Remesa") or ""),
|
||||
"numero_contenedor_505": contenedores if contenedores else "",
|
||||
"tipo_contenedor_505": str(fac.get("Remesa") or ""),
|
||||
"numero_remesa_505": str(fac.get("Remesa") or ""),
|
||||
"tipo_operacion_505": "", # Se llenará en el motor
|
||||
"certificado_origen_505": "S" if fac.get("FungeComoCO") else "N",
|
||||
"numero_certificado_origen_505": "",
|
||||
"numero_exportador_confiable_505": (datos_empresa.get("NUMDEEXPORTADORCONFIABLE") or "") if datos_empresa else "",
|
||||
"incoterm_505": incoterm_full,
|
||||
"metodo_valoracion_505": met_val_str,
|
||||
"vinculacion_505": str(datos_prov.get("VINCULACION") or "0"),
|
||||
"subdivision_505": "S" if fac.get("Subdivision") else "N",
|
||||
"tipo_cambio_usd_505": str(float(fac.get("TipoCambio") or 0)),
|
||||
"se_genero_en_contingencia_505": "S" if fac.get("ModoContingencia") else "N",
|
||||
"operacion_cove_505": fac.get("NumOperacionVU") or "",
|
||||
"transportista_clave_505": fac.get("Transportista") or "",
|
||||
"rfc_505": (datos_trans.get("RFC") or "") if datos_trans else "",
|
||||
"caat_505": (datos_trans.get("CODIGOCAAT") or "") if datos_trans else "",
|
||||
"id_pedimento": id_pedimento,
|
||||
"PaisFactura_505": datos_prov.get("PAIS", ""),
|
||||
"Bultos_cantidad_505": str(fac.get("CantBultos", 0)),
|
||||
"provcomp_numregidtrib_505": " ",
|
||||
"provcomp_rfc_505": " ",
|
||||
"provcomp_curp_505": " ",
|
||||
"provcomp_nombre_505": " ",
|
||||
"provcomp_calle_505": " ",
|
||||
"provcomp_numero_exterior_505": " ",
|
||||
"provcomp_numero_interior_505": " ",
|
||||
"provcomp_colonia_505": " ",
|
||||
"provcomp_localidad_505": " ",
|
||||
"provcomp_municipio_505": " ",
|
||||
"provcomp_estado_505": " ",
|
||||
"provcomp_pais_505": " ",
|
||||
"provcomp_codigo_postal_505": " ",
|
||||
"destinatario_clave_505": " ",
|
||||
"destinatario_numregidtrib_505": " ",
|
||||
"destinatario_rfc_505": " ",
|
||||
"destinatario_curp_505": " ",
|
||||
"destinatario_nombre_505": " ",
|
||||
"destinatario_calle_505": " ",
|
||||
"destinatario_numero_exterior_505": " ",
|
||||
"destinatario_numero_interior_505": " ",
|
||||
"destinatario_colonia_505": " ",
|
||||
"destinatario_localidad_505": " ",
|
||||
"destinatario_referencia_505": " ",
|
||||
"destinatario_municipio_505": " ",
|
||||
"destinatario_estado_505": " ",
|
||||
"destinatario_pais_505": " ",
|
||||
"destinatario_codigo_postal_505": " ",
|
||||
"uuid_505": " ",
|
||||
"path_pdf_cfdi_505": " ",
|
||||
"path_xml_cfdi_505": " ",
|
||||
"estatus_cove_505": " "
|
||||
}
|
||||
|
||||
return registro
|
||||
144
registros/scaii/reg_551.py
Normal file
144
registros/scaii/reg_551.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class Generador551SCAII:
|
||||
@staticmethod
|
||||
def preparar_json(par: Dict[str, Any], id_factura: int, datos_parte: Dict[str, Any],
|
||||
um_data: Optional[Dict[str, Any]], met_val: str, cla_mon: str,
|
||||
num_remesa: int, num_partida_remesa: int, tipo_moneda_fac: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 551 (Partidas) - SCAII.
|
||||
Implementa paridad total con la lógica de Clarion/WinDev.
|
||||
"""
|
||||
|
||||
# 1. Lógica de Fracción (Regla Octava)
|
||||
frac_raw = par.get("Fraccion", "")
|
||||
frac_oct = par.get("FraccionOctava", "")
|
||||
frac_final = frac_oct if frac_oct else frac_raw
|
||||
|
||||
# 2. Descripciones (Limpieza de CR/LF como en WinDev)
|
||||
desc_esp = par.get("Descripcion", " ").replace("\r", "").replace("\n", "").replace("\t", "")
|
||||
desc_ing = " " # Por ahora
|
||||
|
||||
# 3. Arancel (Mapping de Clarion)
|
||||
tipo_frac = par.get("TipoFracc", "")
|
||||
arancel_map = {"TLCS": 3, "GENERAL": 0, "PROSEC": 1}
|
||||
id_arancel = str(arancel_map.get(tipo_frac, 0))
|
||||
|
||||
# 4. Lógica de Valor Comercial (Loc_ValorExpoMC)
|
||||
# ME = ValorDolares, MN = ValorPesos, else ValorComercial
|
||||
if tipo_moneda_fac == "ME":
|
||||
valor_comercial = float(par.get("ValorDolares") or 0)
|
||||
elif tipo_moneda_fac == "MN":
|
||||
valor_comercial = float(par.get("ValorPesos") or 0)
|
||||
else:
|
||||
valor_comercial = float(par.get("ValorComercial") or 0)
|
||||
|
||||
# 5. Unidad de Medida OMA y Comercial (Mapeo reverso para WinDev)
|
||||
oma_final = str(par.get("ClaveOMA") or " ").strip()
|
||||
oma_raw = str(par.get("UnidadMedida", "")).strip().upper()
|
||||
|
||||
if not oma_final or oma_final == "None":
|
||||
oma_final = "C62_1" if oma_raw in ["6", "PZA"] else oma_raw
|
||||
|
||||
map_um_num = {
|
||||
"PZA": "6", "PIEZA": "6", "PZ": "6", "PC": "6", "PIEZAS": "6",
|
||||
"KGS": "1", "KGM": "1", "KG": "1", "KILOS": "1", "KILO": "1",
|
||||
"LTS": "5", "L": "5", "LITROS": "5", "LITRO": "5",
|
||||
"MTR": "2", "M": "2", "METROS": "2", "METRO": "2",
|
||||
"PAR": "7", "PARES": "7",
|
||||
"CJ": "3", "CAJA": "3", "CAJAS": "3",
|
||||
"JGO": "12", "JUEGO": "12", "SET": "12", "KIT": "12"
|
||||
}
|
||||
|
||||
# Priorizar la clave que venga de la base de datos (CLAVEOMA)
|
||||
if um_data and um_data.get("CLAVEOMA"):
|
||||
um_comercial_num = str(um_data["CLAVEOMA"]).strip()
|
||||
else:
|
||||
um_comercial_num = map_um_num.get(oma_raw, oma_raw)
|
||||
|
||||
# 6. Cálculo del Valor Agregado
|
||||
valor_agregado_me = float(par.get("ValorAgregadoME") or 0)
|
||||
valor_aduanas_me = float(par.get("ValorAduanasME") or 0)
|
||||
valor_dolares = float(par.get("ValorDolares") or 0)
|
||||
valor_aduanas_mn = float(par.get("ValorAduanasMN") or 0)
|
||||
valor_pesos = float(par.get("ValorPesos") or 0)
|
||||
costo_auxiliar_me = float(par.get("CostoAuxiliarME") or 0)
|
||||
cantidad = float(par.get("Cantidad") or 0)
|
||||
|
||||
# 1ra Opción (EXPO): Si es Exportación, el valor ya viene calculado en ValorAgregadoME
|
||||
if valor_agregado_me > 0:
|
||||
valor_agregado = valor_agregado_me
|
||||
# 2da Opción (IMPO - Exactitud WinDev): Costo Unitario Auxiliar * Cantidad
|
||||
elif costo_auxiliar_me > 0:
|
||||
valor_agregado = costo_auxiliar_me * cantidad
|
||||
# 3ra Opción: Si Valor Aduanas ME existe
|
||||
elif valor_aduanas_me > 0 and valor_aduanas_me > valor_dolares:
|
||||
valor_agregado = valor_aduanas_me - valor_dolares
|
||||
# 4ta Opción: Calcular mediante el tipo de cambio implícito
|
||||
elif valor_aduanas_mn > 0 and valor_aduanas_mn > valor_pesos and valor_dolares > 0:
|
||||
tc_implicito = valor_pesos / valor_dolares if valor_dolares else 1
|
||||
valor_agregado = (valor_aduanas_mn - valor_pesos) / tc_implicito
|
||||
else:
|
||||
valor_agregado = 0
|
||||
|
||||
valor_agregado = round(max(0, valor_agregado), 2)
|
||||
|
||||
# 7. Método de Valoración específico por partida
|
||||
metodo_val = str(par.get("MetodoValoracion") or "").strip()
|
||||
if not metodo_val:
|
||||
metodo_val = str(met_val) if met_val else ""
|
||||
|
||||
registro = {
|
||||
"id_factura": id_factura,
|
||||
"fraccion_551": str(frac_final),
|
||||
"descripcion_mercancia_551": desc_esp[:250],
|
||||
"numero_parte_551": str(par.get("NumParte") or " "),
|
||||
"valor_mercancia_551": valor_comercial,
|
||||
"cantidad_comercial_551": float(par.get("Cantidad") or 0),
|
||||
"unidad_medida_comercial_551": um_comercial_num, # Ej. "6"
|
||||
"cantidad_tarifa_551": float(par.get("Cantidad") or 0), # Se ajusta en el motor
|
||||
"umt_551": " ", # WinDev envía un espacio vacío aquí
|
||||
"valor_agregado_551": str(valor_agregado),
|
||||
"vinculacion_551": "0",
|
||||
"metodo_valoracion_551": metodo_val,
|
||||
"marca_551": str(par.get("Marca") or " ").strip() or " ",
|
||||
"modelo_551": str(par.get("Modelo") or " ").strip() or " ",
|
||||
"pais_origen_destino_551": str(par.get("PaisOrigen") or " "),
|
||||
"pais_comprador_vendedor_551": "MEX", # Generalmente MEX en Impo
|
||||
"entidad_federativa_origen_551": " ",
|
||||
"entidad_federativa_destino_551": " ",
|
||||
"entidad_federativa_comprador_551": " ",
|
||||
"entidad_federativa_vendedor_551": " ",
|
||||
"identificador_arancel_aplicar_551": id_arancel,
|
||||
"clave_industria_551": str(par.get("Sector") or " "),
|
||||
"peso_bruto_551": float(par.get("PesoBruto") or 0),
|
||||
"uso_mercancia_551": " ",
|
||||
"estado_mercancia_551": " ",
|
||||
"moneda_partida_551": str(cla_mon),
|
||||
"numero_factura_551": str(par.get("NumFactura") or " "),
|
||||
"tipo_bulto_551": " ", # Se llena en motor si aplica
|
||||
"cantidad_bultos_551": float(par.get("CantBultos") or 0),
|
||||
|
||||
# --- PARIDAD WINDEV ---
|
||||
"peso_bruto_kg_551": float(par.get("PesoNeto") or 0), # WinDev envía Peso Neto aquí
|
||||
"peso_neto_kg_551": float(par.get("ValorAduanasMN") or 0), # HACK WinDev: Envía ValorAduanasMN aquí
|
||||
|
||||
"numero_remesa_551": str(num_remesa or " "),
|
||||
"numero_partida_remesa_551": str(par.get("Linea") or num_partida_remesa),
|
||||
"cantidad_alterna_551": float(par.get("CantidadAlterna") or 0),
|
||||
"unidad_medida_alterna_551": str(par.get("UnidadAlterna") or " "),
|
||||
"descripcion_ingles_551": str(par.get("DescripcionIngles") or " ").strip()[:80],
|
||||
"identificador_sistema_scaii_scaf_551": "SCAII",
|
||||
"vu_submodelo_551": " ",
|
||||
"vu_serie_551": " ",
|
||||
"valor_unitario_aduana_551": float(par.get("CostoUnitarioME") or 0),
|
||||
"valor_dolares_551": float(par.get("ValorDolares") or 0),
|
||||
"unidad_medida_oma_551": oma_final, # Código de la fracción (ej. '6')
|
||||
"valor_dolares_cove_551": "0",
|
||||
"subdivision_fraccion_551": str(frac_final)[-2:] if len(str(frac_final)) >= 2 else "00",
|
||||
"codigo_carta_porte_551": " ",
|
||||
"nodo_551": "85858585",
|
||||
"CantidadComercialOMA_551": str(float(par.get("Cantidad") or 0))
|
||||
}
|
||||
|
||||
return registro
|
||||
23
registros/scaii/reg_554.py
Normal file
23
registros/scaii/reg_554.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador554SCAII:
|
||||
@staticmethod
|
||||
def preparar_json(iden: Dict[str, Any], id_partida: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 554 (Identificadores de Partidas) - SCAII.
|
||||
"""
|
||||
registro = {
|
||||
"tipo_caso_554": iden.get("identificador", " "),
|
||||
"complemento_caso_1_554": iden.get("complemento_1", " "),
|
||||
"complemento_caso_2_554": iden.get("complemento_2", " "),
|
||||
"complemento_caso_3_554": iden.get("complemento_2", " "), # WinDev repite C3 con C3, pero aquí mapeamos C1, C2, C3
|
||||
"id_partida": id_partida
|
||||
}
|
||||
|
||||
# En el código WinDev: FormURL_Reg554.complemento_caso_3_554 = DS_IntPar.C3
|
||||
# Pero DS_IntPar.C3 es COMPLEMENTO2. DS_IntPar.C4 es COMPLEMENTO3.
|
||||
# Ajustaré para que use el complemento_3 real si existe.
|
||||
if iden.get("complemento_3"):
|
||||
registro["complemento_caso_3_554"] = iden.get("complemento_3")
|
||||
|
||||
return registro
|
||||
18
registros/scaii/reg_558.py
Normal file
18
registros/scaii/reg_558.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class Generador558:
|
||||
@staticmethod
|
||||
def preparar_json(id_partida: int, observaciones: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Lógica para Registro 558 (Observaciones de Partidas).
|
||||
Limpia caracteres especiales como saltos de línea y tabuladores.
|
||||
"""
|
||||
if not observaciones:
|
||||
observaciones = " "
|
||||
|
||||
obs_limpia = observaciones.replace("\r", "").replace("\n", "").replace("\t", "").strip()
|
||||
|
||||
return {
|
||||
"observaciones_558": obs_limpia[:1000],
|
||||
"id_partida": id_partida
|
||||
}
|
||||
Reference in New Issue
Block a user