Se agregaron los moduloes de api_v2

This commit is contained in:
2025-10-03 23:16:47 -06:00
parent ac075bfeb7
commit 7149515606
60 changed files with 3714 additions and 252 deletions

View File

@@ -0,0 +1 @@
from . import tasks

View File

@@ -0,0 +1,379 @@
from controllers.RESTController import APIRESTController
from controllers.SOAPController import VUCEMController
from typing import List, Dict, Any
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import List, Dict
class PedimentoController(APIRESTController):
def __init__(self):
super().__init__()
async def put_pedimento(self, pedimento_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para actualizar un pedimento en la API.
"""
return await self._make_request_async('PUT', f'customs/pedimentos/{pedimento_id}/', data=data)
async def post_edocument(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para enviar un documento digitalizado a la API.
Args:
data: Diccionario con los datos del documento a enviar
"""
return await self._make_request_async('POST', 'customs/edocuments/', data=data)
async def post_cove(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para enviar un número de COVE a la API.
Args:
data: Diccionario con los datos del COVE a enviar
"""
return await self._make_request_async('POST', 'customs/coves/', data=data)
async def put_edocument(self, edocument_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para actualizar un documento digitalizado en la API.
Args:
edocument_id: UUID del documento a actualizar
data: Diccionario con los datos a actualizar
"""
return await self._make_request_async('PUT', f'customs/edocuments/{edocument_id}/', data=data)
class PedimentoVUController(VUCEMController):
def __init__(self):
super().__init__() # Implementación específica para Coves VU
def generate_remesas_template(self, username: str, password: str, aduana: str, patente: str, numero_operacion: str, pedimento: str) -> str:
"""
Genera el template SOAP para consultar remesas
Args:
username: Usuario de VUCEM
password: Contraseña de VUCEM
aduana: Código de aduana
patente: Número de patente
Returns:
str: Template SOAP XML completo
"""
soap_template = f'''
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:con="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarremesas"
xmlns:com="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>{username}</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{password}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<con:consultarRemesasPeticion>
<con:numeroOperacion>{numero_operacion}</con:numeroOperacion>
<con:peticion>
<com:aduana>{aduana}</com:aduana>
<com:patente>{patente}</com:patente>
<com:pedimento>{pedimento}</com:pedimento>
</con:peticion>
</con:consultarRemesasPeticion>
</soapenv:Body>
</soapenv:Envelope>'''
return soap_template
def generate_pedimento_completo_template(self, username: str, password: str, aduana: str, patente: str, pedimento: str) -> str:
"""
Genera el template SOAP para consultar pedimento completo
Args:
username: Usuario de VUCEM
password: Contraseña de VUCEM
aduana: Código de aduana
patente: Número de patente
pedimento: Número de pedimento
Returns:
str: Template SOAP XML completo
"""
soap_template = f'''<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:con="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto"
xmlns:com="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>{username}</wsse:Username>
<wsse:Password
Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{password}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<con:consultarPedimentoCompletoPeticion>
<con:peticion>
<com:aduana>{aduana}</com:aduana>
<com:patente>{patente}</com:patente>
<com:pedimento>{pedimento}</com:pedimento>
</con:peticion>
</con:consultarPedimentoCompletoPeticion>
</soapenv:Body>
</soapenv:Envelope>'''
return soap_template
# Pedimento Completo
@dataclass
class PedimentoXMLScraper: # Clase me extrae datos de Pedimento
"""
Clase para manejar la extracción de datos de un XML.
"""
def _get_numero_operacion(self, root: ET.Element) -> str:
"""
Método para obtener el número de operación del XML.
Args:
root: Elemento raíz del XML.
Returns:
Número de operación como string.
"""
numero_operacion = root.find('.//ns2:numeroOperacion', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
return numero_operacion.text if numero_operacion is not None else None
def _get_pedimento(self, root: ET.Element) -> str:
"""
Método para obtener el pedimento del XML.
Args:
root: Elemento raíz del XML.
Returns:
Pedimento como string.
"""
pedimento = root.find('.//ns2:pedimento/ns2:pedimento', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
return pedimento.text if pedimento is not None else None
def _get_curp_apoderado(self, root: ET.Element) -> str:
"""
Método para obtener el CURP del apoderado del XML.
Args:
root: Elemento raíz del XML.
Returns:
CURP del apoderado como string.
"""
curp_apoderado = root.find('.//ns2:curpApoderadomandatario', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
return curp_apoderado.text if curp_apoderado is not None else None
def _get_agente_aduanal(self, root: ET.Element) -> str:
"""
Método para obtener el RFC del agente aduanal del XML.
Args:
root: Elemento raíz del XML.
Returns:
RFC del agente aduanal como string.
"""
agente_aduanal = root.find('.//ns2:rfcAgenteAduanalSocFactura', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
return agente_aduanal.text if agente_aduanal is not None else None
def _get_partidas(self, root: ET.Element) -> int:
"""
Método para obtener el número máximo de partidas del XML.
Args:
root: Elemento raíz del XML.
Returns:
Número máximo de partidas como entero.
"""
partidas_elements = root.findall('.//ns2:partidas', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
partidas_values = []
for elem in partidas_elements:
try:
if elem.text is not None:
partidas_values.append(int(elem.text))
except ValueError:
continue
return max(partidas_values) if partidas_values else None
def _get_identificadores_ed(self, root: ET.Element) -> list:
"""
Método para obtener todos los identificadores con clave 'ED' del XML.
Args:
root: Elemento raíz del XML.
Returns:
Lista de diccionarios con los datos de identificadores ED.
"""
namespaces = {
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
'ns': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes'
}
identificadores_ed = []
# Buscar todos los elementos identificadores
identificadores_elements = root.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
for identificador in identificadores_elements:
try:
# Extraer la clave del identificador (está dentro de claveIdentificador con namespace)
clave_elem = identificador.find('ns:claveIdentificador/ns:clave', namespaces)
clave = clave_elem.text if clave_elem is not None else None
# Solo procesar si la clave es 'ED'
if clave == 'ED':
# Extraer descripción (con namespace)
descripcion_elem = identificador.find('ns:claveIdentificador/ns:descripcion', namespaces)
descripcion = descripcion_elem.text if descripcion_elem is not None else None
# Extraer complemento1 (con namespace)
complemento1_elem = identificador.find('ns:complemento1', namespaces)
complemento1 = complemento1_elem.text if complemento1_elem is not None else None
# Agregar a la lista si tenemos los datos básicos
if clave and complemento1:
identificadores_ed.append({
'clave': clave,
'descripcion': descripcion,
'complemento1': complemento1
})
except Exception as e:
# Log del error pero continuar procesando otros identificadores
print(f"Error procesando identificador: {e}")
continue
return identificadores_ed
def _remesas(self, root: ET.Element) -> bool:
"""
Método para verificar si el pedimento tiene remesas.
Busca identificadores con clave 'RC' (REMESAS DE CONSOLIDADO).
Args:
root: Elemento raíz del XML.
Returns:
True si encuentra identificadores con clave 'RC', False en caso contrario.
"""
namespaces = {
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
'ns': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes'
}
# Buscar todos los elementos identificadores
identificadores_elements = root.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
for identificador in identificadores_elements:
try:
# Extraer la clave del identificador
clave_elem = identificador.find('ns:claveIdentificador/ns:clave', namespaces)
clave = clave_elem.text if clave_elem is not None else None
# Si encontramos una clave 'RC', el pedimento tiene remesas
if clave == 'RC':
return True
except Exception as e:
# Log del error pero continuar procesando otros identificadores
print(f"Error procesando identificador para remesas: {e}")
continue
print("No se encontraron remesas (sin identificadores RC)")
return False
def _get_tipo_operacion(self, root: ET.Element) -> str:
"""
Método para obtener el tipo de operación del XML.
Args:
root: Elemento raíz del XML.
Returns:
Tipo de operación como string.
"""
tipo_operacion = root.find('.//ns2:tipoOperacion/ns2:clave', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
return tipo_operacion.text if tipo_operacion is not None else None
def _get_cove(self, root: ET.Element) -> str:
"""
Método para obtener el número de COVE del XML.
Args:
root: Elemento raíz del XML.
Returns:
Número de COVE como string.
"""
namespaces = {
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
'ns': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes'
}
facturas = root.findall('.//ns2:facturas', namespaces=namespaces)
coves = []
for factura in facturas:
cove = factura.find('ns2:numero', namespaces)
if cove is not None:
coves.append(cove.text)
else:
print("No se encontró <ns2:numero> en la factura.")
return coves if coves else None
def extract_data(self, xml_content: str) -> dict:
"""
Método para extraer datos específicos del XML.
Args:
xml_content: Contenido del XML como string.
Returns:
Diccionario con los datos extraídos.
"""
try:
root = ET.fromstring(xml_content)
# Extraer datos con manejo de errores individual
data = {}
data['numero_operacion'] = self._get_numero_operacion(root)
data['pedimento'] = self._get_pedimento(root)
data['curp_apoderado'] = self._get_curp_apoderado(root)
data['agente_aduanal'] = self._get_agente_aduanal(root)
data['numero_partidas'] = self._get_partidas(root)
data['identificadores_ed'] = self._get_identificadores_ed(root)
data['remesas'] = self._remesas(root)
data['tipo_operacion'] = self._get_tipo_operacion(root)
data['coves'] = self._get_cove(root)
# Verificar que se extrajeron los datos esenciales
if not any([data['numero_operacion'], data['pedimento'], data['curp_apoderado'], data['agente_aduanal'], data['coves']]):
return {}
return data
except ET.ParseError as e:
print(f"Error al parsear el XML: {e}")
return {}
except Exception as e:
print(f"Error inesperado al extraer datos del XML: {e}")
return {}
pedimento_rest_controller = PedimentoController()
pedimento_vu_controller = PedimentoVUController()
pedimento_xml_scraper = PedimentoXMLScraper()

View File

@@ -0,0 +1,21 @@
from fastapi import APIRouter, BackgroundTasks, status, HTTPException
from fastapi.responses import JSONResponse
from .schemas import PedimentoCompletoRequestSchema
from .tasks import process_pedimento_completo_request
import logging
logger = logging.getLogger("app.api")
router = APIRouter()
@router.post("/services/pedimento_completo", status_code=status.HTTP_202_ACCEPTED)
async def download_pedimento_completo(Pedimento: PedimentoCompletoRequestSchema):
"""
Endpoint para iniciar la descarga completa de un pedimento.
"""
pedimento_dict = Pedimento.model_dump()
# Ejecuta la tarea de Celery de forma asíncrona
task = process_pedimento_completo_request.delay(pedimento_dict)
# Puedes devolver el ID de la tarea para consultar el estado después
return {"status": "submitted", "detail": "La solicitud de descarga del pedimento completo ha sido enviada.", "task_id": task.id}

View File

@@ -1,24 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class PedimentoBaseSchema(BaseModel):
id: str
pedimento: str
pedimento_app: str
aduana: str
patente: str
regimen: str
organizacion: str
clave_pedimento: str
fecha_pago: Optional[str]
fecha_inicio: Optional[str]
fecha_fin: Optional[str]
alerta: Optional[bool]
agente_aduanal: Optional[str]
curp_apoderado: Optional[str]
importe_total: Optional[float]
saldo_disponible: Optional[float]
importe_pedimento: Optional[float]
existe_expediente: Optional[bool]

View File

@@ -0,0 +1,95 @@
from typing import Optional, Union, Dict, Any
from uuid import UUID
from datetime import datetime
# CORRECCIÓN CLAVE: Se importa el 'validator' para que el decorador funcione
from pydantic import BaseModel, Field, validator
from schemas.CredencialSchema import CredencialBaseSchema
class PedimentoBaseSchema(BaseModel):
id: str = Field(..., description="ID único del pedimento")
pedimento: str = Field(..., description="Número de pedimento")
pedimento_app: str = Field(..., description="Número de pedimento en la aplicación")
aduana: str = Field(..., description="Aduana asociada al pedimento")
patente: str = Field(..., description="Patente asociada al pedimento")
numero_operacion: str = Field(None, description="Número de operación del pedimento")
# Usamos Field(None, ...) para campos Optional
regimen: Optional[str] = Field(None, description="Régimen aduanero del pedimento")
organizacion: str = Field(..., description="Organización asociada al pedimento")
clave_pedimento: Optional[str] = Field(None, description="Clave del pedimento")
fecha_pago: Optional[str] = Field(None, description="Fecha de pago del pedimento")
fecha_inicio: Optional[str] = Field(None, description="Fecha de inicio del pedimento")
fecha_fin: Optional[str] = Field(None, description="Fecha de fin del pedimento")
alerta: Optional[bool] = Field(None, description="Indica si hay alerta en el pedimento")
agente_aduanal: Optional[str] = Field(None, description="Agente aduanal asociado al pedimento")
curp_apoderado: Optional[str] = Field(None, description="CURP del apoderado")
importe_total: Optional[float] = Field(None, description="Importe total del pedimento")
saldo_disponible: Optional[float] = Field(None, description="Saldo disponible del pedimento")
importe_pedimento: Optional[float] = Field(None, description="Importe del pedimento")
existe_expediente: Optional[bool] = Field(None, description="Indica si existe expediente")
# Validadores de Pydantic v1 (usando @validator)
@validator('id')
def validate_id(cls, v):
if not v or not isinstance(v, str):
raise ValueError('id must be a non-empty string')
return v
@validator('pedimento')
def validate_pedimento(cls, v):
if not v or not isinstance(v, str):
raise ValueError('pedimento must be a non-empty string')
return v
@validator('pedimento_app')
def validate_pedimento_app(cls, v):
if not v or not isinstance(v, str):
raise ValueError('pedimento_app must be a non-empty string')
return v
@validator('aduana')
def validate_aduana(cls, v):
if not v or not isinstance(v, str):
raise ValueError('aduana must be a non-empty string')
return v
@validator('patente')
def validate_patente(cls, v):
if not v or not isinstance(v, str):
raise ValueError('patente must be a non-empty string')
return v
@validator('organizacion')
def validate_organizacion(cls, v):
if not v or not isinstance(v, str):
raise ValueError('organizacion must be a non-empty string')
return v
# Validadores combinados para campos opcionales
@validator('fecha_pago', 'fecha_inicio', 'fecha_fin', 'agente_aduanal', 'curp_apoderado', 'regimen', 'clave_pedimento', pre=True)
def validate_optional_strings(cls, v):
if v is not None and not isinstance(v, str):
raise ValueError('Campo opcional debe ser string o None')
return v
@validator('alerta', 'existe_expediente', pre=True)
def validate_optional_bools(cls, v):
if v is not None and not isinstance(v, bool):
raise ValueError('Campo opcional debe ser booleano o None')
return v
@validator('importe_total', 'saldo_disponible', 'importe_pedimento', pre=True)
def validate_optional_numbers(cls, v):
if v is not None and not isinstance(v, (float, int)):
raise ValueError('Campo opcional debe ser numérico o None')
class PedimentoCompletoRequestSchema(BaseModel):
pedimento: PedimentoBaseSchema
credencial: CredencialBaseSchema

View File

@@ -0,0 +1,346 @@
"""Servicios para el manejo de pedimentos completos."""
import logging
from typing import Any, Dict, List, Optional
from fastapi import HTTPException
# Importar controladores (nota: el archivo se llama controllers,py con coma)
import sys
import os
sys.path.append(os.path.dirname(__file__))
from .controllers import pedimento_rest_controller, pedimento_vu_controller, pedimento_xml_scraper
from utils.helpers import soap_error
# Logger configurado para el módulo
logger = logging.getLogger("app.api")
async def consume_ws_get_pedimento_completo(**kwargs) -> Dict[str, Any]:
"""
Consume el servicio web para obtener pedimento completo.
Args:
**kwargs: Debe contener 'credencial' y 'pedimento' con sus respectivos campos
Returns:
Dict con 'documento' y 'xml_content'
Raises:
HTTPException: Si hay errores en la petición o datos faltantes
"""
# Validar datos de entrada
credencial = kwargs.get('credencial', {})
pedimento_data = kwargs.get('pedimento', {})
if not credencial.get('user') or not credencial.get('password'):
raise HTTPException(status_code=400, detail="Credenciales incompletas")
required_fields = ['aduana', 'patente', 'pedimento', 'id', 'organizacion']
missing_fields = [f for f in required_fields if not pedimento_data.get(f)]
if missing_fields:
raise HTTPException(
status_code=400,
detail=f"Datos de pedimento incompletos: {missing_fields}"
)
logger.info(f"Iniciando consulta SOAP para pedimento: {pedimento_data.get('pedimento')}")
try:
# Generar XML SOAP
soap_xml = pedimento_vu_controller.generate_pedimento_completo_template(
username=credencial.get('user'),
password=credencial.get('password'),
aduana=pedimento_data.get('aduana'),
patente=pedimento_data.get('patente'),
pedimento=pedimento_data.get('pedimento')
)
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8'
}
# Realizar petición SOAP
soap_response = await pedimento_vu_controller.make_request_async(
"ventanilla-ws-pedimentos/ConsultarPedimentoCompletoService?wsdl",
data=soap_xml,
headers=soap_headers
)
if not soap_response:
raise HTTPException(status_code=500, detail="No se recibió respuesta del servicio SOAP")
if soap_error(soap_response):
logger.error(f"Error en respuesta SOAP: {soap_response.text if hasattr(soap_response, 'text') else 'Sin detalles'}")
raise HTTPException(status_code=500, detail="Error en la respuesta del servicio SOAP")
# Extraer datos del XML
try:
data = pedimento_xml_scraper.extract_data(soap_response.text)
except Exception as e:
logger.error(f"Error al extraer datos XML: {e}")
raise HTTPException(status_code=500, detail="Error al procesar respuesta XML")
# Generar nombre de archivo
file_name = f"vu_PC_{pedimento_data.get('pedimento_app', 'unknown')}.xml"
# Enviar documento
try:
document_response = await pedimento_rest_controller.post_document(
soap_response=soap_response,
organizacion=pedimento_data.get('organizacion'),
pedimento=pedimento_data.get('id'),
file_name=file_name,
document_type=2,
)
except Exception as e:
logger.error(f"Error al enviar documento: {e}")
raise HTTPException(status_code=500, detail="Error al guardar documento")
# Enriquecer datos con información del pedimento
data['organizacion'] = pedimento_data.get('organizacion')
data['id'] = pedimento_data.get('id')
logger.info(f"Pedimento completo procesado exitosamente: {pedimento_data.get('pedimento')}")
return {
"documento": document_response,
"xml_content": data
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error inesperado en consume_ws_get_pedimento_completo: {e}")
raise HTTPException(status_code=500, detail=f"Error interno: {str(e)}")
async def put_pedimento_data(**kwargs) -> Dict[str, Any]:
"""
Actualiza la información del pedimento en el sistema REST.
Args:
**kwargs: Datos de credencial y pedimento
Returns:
Dict con resultados del procesamiento
Raises:
HTTPException: Si hay errores críticos en el procesamiento
"""
# Inicializar variables de respuesta
result = {
"documento": None,
"pedimento_actualizado": None,
"coves_procesados": None,
"coves_error": None,
"edocuments_procesados": None,
"edocuments_error": None,
"xml_content": None
}
# Obtener datos del servicio web
try:
ws_data = await consume_ws_get_pedimento_completo(**kwargs)
result["documento"] = ws_data.get("documento", None)
xml_content = ws_data.get('xml_content', {})
result["xml_content"] = xml_content
if not xml_content:
logger.warning("No se obtuvo contenido XML del servicio web")
return result
except HTTPException:
raise # Re-lanzar HTTPExceptions
except Exception as e:
logger.error(f"Error inesperado al consumir servicio web: {e}")
raise HTTPException(status_code=500, detail=f"Error al obtener datos del pedimento: {str(e)}")
# Actualizar información del pedimento (crítico)
try:
result["pedimento_actualizado"] = await _update_pedimento_info(kwargs, xml_content)
except Exception as e:
logger.error(f"Error crítico al actualizar pedimento: {e}")
raise HTTPException(status_code=500, detail=f"Error al actualizar el pedimento: {str(e)}")
# Procesar COVEs (no crítico)
try:
result["coves_procesados"] = await _process_coves_safely(kwargs, xml_content)
except Exception as e:
logger.warning(f"Error al procesar COVEs: {e}")
result["coves_error"] = str(e)
# Procesar documentos digitalizados (no crítico)
try:
result["edocuments_procesados"] = await _process_edocuments_safely(kwargs, xml_content)
except Exception as e:
logger.warning(f"Error al procesar documentos digitalizados: {e}")
result["edocuments_error"] = str(e)
logger.info("Procesamiento de pedimento completo finalizado")
return result
async def _update_pedimento_info(kwargs: Dict[str, Any], xml_content: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Actualiza la información del pedimento.
Args:
kwargs: Datos originales
xml_content: Contenido XML extraído
Returns:
Respuesta del servicio de actualización
"""
if not xml_content:
logger.info("No hay contenido XML para actualizar el pedimento")
return None
# Preparar datos para actualización (excluir identificadores_ed)
update_content = {k: v for k, v in xml_content.items() if k != 'identificadores_ed'}
update_content['existe_expediente'] = True
pedimento_id = kwargs.get('pedimento', {}).get('id')
if not pedimento_id:
raise ValueError("ID de pedimento no encontrado para actualización")
response = await pedimento_rest_controller.put_pedimento(pedimento_id, update_content)
logger.info(f"Pedimento {pedimento_id} actualizado exitosamente")
return response
async def _process_coves_safely(kwargs: Dict[str, Any], xml_content: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
"""
Procesa los COVEs de manera segura.
"""
coves = xml_content.get('coves', [])
if not coves:
logger.info("No se encontraron COVEs para procesar")
return None
logger.info(f"Procesando {len(coves)} COVEs encontrados")
result = await _post_coves(kwargs.get('pedimento', {}), coves)
logger.info(f"Se procesaron exitosamente {len(result)} COVEs")
return result
async def _process_edocuments_safely(kwargs: Dict[str, Any], xml_content: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
"""
Procesa los documentos digitalizados de manera segura.
"""
identificadores_ed = xml_content.get('identificadores_ed', [])
if not identificadores_ed:
logger.info("No se encontraron documentos digitalizados (identificadores ED)")
return None
logger.info(f"Procesando {len(identificadores_ed)} documentos digitalizados...")
result = await _post_edocuments(kwargs.get('pedimento', {}), identificadores_ed)
logger.info(f"Se procesaron exitosamente {len(result)} documentos digitalizados")
return result
async def _post_coves(pedimento_data: Dict[str, Any], coves: List[str]) -> List[Dict[str, Any]]:
"""
Envía COVEs al sistema REST.
Args:
pedimento_data: Datos del pedimento
coves: Lista de números de COVE
Returns:
Lista de respuestas exitosas
Raises:
HTTPException: Si no se pudo procesar ningún COVE
"""
if not coves:
return []
responses = []
errors = []
for cove in coves:
document_data = {
'numero_cove': cove,
'organizacion': pedimento_data.get('organizacion'),
'pedimento': pedimento_data.get('id')
}
try:
response = await pedimento_rest_controller.post_cove(document_data)
if response:
responses.append(response)
logger.debug(f"COVE {cove} procesado exitosamente")
except Exception as e:
error_msg = f"Error al procesar COVE {cove}: {str(e)}"
logger.warning(error_msg)
errors.append(error_msg)
if not responses and coves:
error_detail = f"No se pudo procesar ningún COVE. Errores: {'; '.join(errors)}"
logger.error(error_detail)
raise HTTPException(status_code=500, detail=error_detail)
if errors:
logger.warning(f"Se procesaron {len(responses)}/{len(coves)} COVEs. Errores: {len(errors)}")
return responses
async def _post_edocuments(pedimento_data: Dict[str, Any], identificadores_ed: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Envía documentos digitalizados al sistema REST.
Args:
pedimento_data: Datos del pedimento
identificadores_ed: Lista de identificadores de documentos
Returns:
Lista de respuestas exitosas
Raises:
HTTPException: Si no se pudo procesar ningún documento
"""
if not identificadores_ed:
return []
responses = []
errors = []
for identificador in identificadores_ed:
try:
# Validar campos requeridos
if not identificador.get('clave') or not identificador.get('complemento1'):
logger.warning(f"Documento con datos incompletos omitido: {identificador}")
continue
document_data = {
'clave': identificador.get('clave'),
'descripcion': identificador.get('descripcion', ''),
'numero_edocument': identificador.get('complemento1'),
'organizacion': pedimento_data.get('organizacion'),
'pedimento': pedimento_data.get('id')
}
response = await pedimento_rest_controller.post_edocument(document_data)
if response:
responses.append(response)
logger.debug(f"Documento {identificador.get('clave')} procesado exitosamente")
except Exception as e:
error_msg = f"Error al procesar documento {identificador.get('clave', 'unknown')}: {str(e)}"
logger.warning(error_msg)
errors.append(error_msg)
if not responses and identificadores_ed:
error_detail = f"No se pudo procesar ningún documento digitalizado. Errores: {'; '.join(errors)}"
logger.error(error_detail)
raise HTTPException(status_code=500, detail=error_detail)
if errors:
logger.warning(f"Se procesaron {len(responses)}/{len(identificadores_ed)} documentos. Errores: {len(errors)}")
return responses

View File

@@ -0,0 +1,26 @@
from celery_app import celery_app
from .services import put_pedimento_data
import asyncio # Necesario para ejecutar funciones async dentro de Celery
@celery_app.task(bind=True)
def process_pedimento_completo_request(self, pedimento_data: dict):
"""
Tarea de Celery para procesar la descarga de un solo documento edoc.
"""
try:
# Ejecutar la función asíncrona dentro del hilo síncrono de Celery
loop = asyncio.get_event_loop()
result = loop.run_until_complete(put_pedimento_data(**pedimento_data))
return {"status": "success", "result": result}
except Exception as e:
# Manejo de errores
self.update_state(
state='FAILURE',
meta={'exc_type': type(e).__name__, 'exc_message': str(e)}
)
# Es crucial volver a lanzar la excepción para que Celery la marque como fallida
raise e