diff --git a/api/customs/tasks/auditoria.py b/api/customs/tasks/auditoria.py index 936ec88..e0ff4e0 100644 --- a/api/customs/tasks/auditoria.py +++ b/api/customs/tasks/auditoria.py @@ -1,3 +1,6 @@ +import os +from datetime import datetime +from django.db import models from celery import shared_task, group from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument from core.utils import xml_controller @@ -198,7 +201,7 @@ def auditar_coves(organizacion_id): pedimento, servicio=8, related_name='coves', - variable='acuse_descargado', + variable='cove_descargado', mensaje='COVE' ) @@ -247,7 +250,7 @@ def auditar_cove_por_pedimento(pedimento_id): pedimento, servicio=8, related_name='coves', - variable='acuse_descargado', + variable='cove_descargado', mensaje='COVE' ) return {'success': True, 'pedimento_id': str(pedimento_id)} @@ -302,3 +305,154 @@ def auditar_acuse_por_pedimento(pedimento_id): except Exception as e: return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)} +@shared_task +def auditar_pedimento_por_id(pedimento_id): + """ + Tarea para auditar un pedimento específico verificando todos sus documentos y datos. + """ + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + resultado = { + 'pedimento_id': str(pedimento_id), + 'pedimento': pedimento.pedimento, + 'pedimento_app': pedimento.pedimento_app, + 'organizacion': str(pedimento.organizacion.id), + 'fecha_auditoria': datetime.now().isoformat(), + 'estado_general': 'EN_PROGRESO', + 'detalles': {} + } + + # 1. Verificar documentos XML + from api.record.models import Document + documentos_xml = Document.objects.filter( + pedimento=pedimento, + archivo__endswith='.xml' + ) + + resultado['detalles']['documentos_xml'] = { + 'total': documentos_xml.count(), + 'archivos': [] + } + + for doc in documentos_xml: + try: + xml_info = { + 'id': str(doc.id), + 'nombre': os.path.basename(doc.archivo.name), + 'tamanio': doc.size, + 'extension': doc.extension, + 'tipo': doc.document_type.descripcion if doc.document_type else 'Desconocido' + } + + # Verificar si el archivo existe físicamente + if os.path.exists(doc.archivo.path): + xml_info['existe_fisicamente'] = True + # Intentar leer el XML + try: + with open(doc.archivo.path, 'r', encoding='utf-8') as f: + content = f.read() + xml_info['es_xml_valido'] = ' 0, + 'fecha_pago': bool(pedimento.fecha_pago) + } + + resultado['detalles']['campos_pedimento'] = campos_revisados + resultado['detalles']['campos_completos'] = sum(campos_revisados.values()) + resultado['detalles']['campos_totales'] = len(campos_revisados) + + # 7. Determinar estado general + campos_completos = resultado['detalles']['campos_completos'] + total_campos = resultado['detalles']['campos_totales'] + + if documentos_xml.count() == 0: + resultado['estado_general'] = 'SIN_XML' + resultado['mensaje'] = 'No se encontraron documentos XML' + elif campos_completos == total_campos: + resultado['estado_general'] = 'COMPLETO' + resultado['mensaje'] = 'Pedimento completamente procesado' + elif campos_completos >= total_campos * 0.7: + resultado['estado_general'] = 'PARCIAL' + resultado['mensaje'] = 'Pedimento parcialmente procesado' + else: + resultado['estado_general'] = 'INCOMPLETO' + resultado['mensaje'] = 'Pedimento con información incompleta' + + resultado['porcentaje_completitud'] = (campos_completos / total_campos) * 100 if total_campos > 0 else 0 + + # 8. Sugerencias + sugerencias = [] + if not pedimento.numero_operacion: + sugerencias.append("Falta el número de operación") + if not pedimento.numero_partidas: + sugerencias.append("Falta el número de partidas") + if pedimento.numero_partidas and pedimento.numero_partidas > pedimento.partidas.count(): + sugerencias.append(f"Faltan partidas: {pedimento.numero_partidas - pedimento.partidas.count()} de {pedimento.numero_partidas}") + if not pedimento.contribuyente: + sugerencias.append("Falta el contribuyente asociado") + + resultado['sugerencias'] = sugerencias + + return resultado + + except Pedimento.DoesNotExist: + return { + 'error': f'Pedimento con ID {pedimento_id} no encontrado', + 'pedimento_id': str(pedimento_id) + } + except Exception as e: + return { + 'error': f'Error auditar pedimento {pedimento_id}: {str(e)}', + 'pedimento_id': str(pedimento_id) + } \ No newline at end of file diff --git a/api/customs/tasks/auditoria_xml.py b/api/customs/tasks/auditoria_xml.py new file mode 100644 index 0000000..ac5226c --- /dev/null +++ b/api/customs/tasks/auditoria_xml.py @@ -0,0 +1,194 @@ +# auditoria_xml.py +import xml.etree.ElementTree as ET +from datetime import datetime + +def extraer_info_pedimento_xml(xml_content): + """ + Extrae información específica de un XML de pedimento. + """ + try: + # Parsear el XML + root = ET.fromstring(xml_content) + + # Buscar el namespace (puede variar) + namespaces = { + 'S': 'http://schemas.xmlsoap.org/soap/envelope/', + 'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto', + 'ns3': 'http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta' + } + + resultado = {} + + # Extraer número de operación + num_op = root.find('.//ns2:numeroOperacion', namespaces) + if num_op is not None and num_op.text: + resultado['numero_operacion'] = num_op.text + + # Extraer información del pedimento + pedimento_elem = root.find('.//ns2:pedimento', namespaces) + if pedimento_elem is not None: + # Número de pedimento + ped_num = pedimento_elem.find('ns2:pedimento', namespaces) + if ped_num is not None and ped_num.text: + resultado['numero_pedimento'] = ped_num.text + + # Número de partidas + partidas = pedimento_elem.find('ns2:partidas', namespaces) + if partidas is not None and partidas.text: + try: + resultado['numero_partidas'] = int(partidas.text) + except (ValueError, TypeError): + pass + + # Tipo de operación clave + tipo_op_clave = pedimento_elem.find('.//ns2:tipoOperacion/ns2:clave', namespaces) + if tipo_op_clave is not None and tipo_op_clave.text: + if tipo_op_clave.text.strip() == '1': + + resultado['tipo_operacion'] = 'Importacion' + resultado['tipo_operacion_descripcion'] = 'Indica operacion como Importaciones' + + elif tipo_op_clave.text.strip() == '2': + + resultado['tipo_operacion'] = 'Exportacion' + resultado['tipo_operacion_descripcion'] = 'Indica operacion de exportacion' + + + # Clave del documento (clave_pedimento) + clave_doc = pedimento_elem.find('.//ns2:claveDocumento/ns2:clave', namespaces) + if clave_doc is not None and clave_doc.text: + resultado['clave_pedimento'] = clave_doc.text.strip() + + # Aduana (patente) + aduana = pedimento_elem.find('.//ns2:aduanaEntradaSalida/ns2:clave', namespaces) + if aduana is not None and aduana.text: + resultado['aduana_clave'] = aduana.text.strip() + + # Importador/Exportador + importador = pedimento_elem.find('.//ns2:importadorExportador', namespaces) + if importador is not None: + rfc = importador.find('ns2:rfc', namespaces) + if rfc is not None and rfc.text: + resultado['contribuyente_rfc'] = rfc.text.strip() + + razon_social = importador.find('ns2:razonSocial', namespaces) + if razon_social is not None and razon_social.text: + resultado['contribuyente_nombre'] = razon_social.text.strip() + + # Valor en dólares + valor_dolares = importador.find('ns2:valorDolares', namespaces) + if valor_dolares is not None and valor_dolares.text: + try: + resultado['valor_dolares'] = float(valor_dolares.text) + except (ValueError, TypeError): + pass + + # Aduana de despacho + aduana_despacho = importador.find('ns2:aaduanaDespacho/ns2:clave', namespaces) + if aduana_despacho is not None and aduana_despacho.text: + resultado['aduana_despacho'] = aduana_despacho.text.strip() + + # Encabezado del pedimento + encabezado = pedimento_elem.find('ns2:encabezado', namespaces) + if encabezado is not None: + # Aduana + aduana = encabezado.find('ns2:aduanaEntradaSalida/ns2:clave', namespaces) + if aduana is not None and aduana.text: + resultado['aduana_clave'] = aduana.text.strip() + + # Tipo de cambio + tipo_cambio = encabezado.find('ns2:tipoCambio', namespaces) + if tipo_cambio is not None and tipo_cambio.text: + try: + resultado['tipo_cambio'] = float(tipo_cambio.text) + except (ValueError, TypeError): + pass + + # RFC Agente Aduanal + rfc_agente = encabezado.find('ns2:rfcAgenteAduanalSocFactura', namespaces) + if rfc_agente is not None and rfc_agente.text: + resultado['rfc_agente_aduanal'] = rfc_agente.text.strip() + + # CURP Apoderado + curp_apoderado = encabezado.find('ns2:curpApoderadomandatario', namespaces) + if curp_apoderado is not None and curp_apoderado.text: + resultado['curp_apoderado'] = curp_apoderado.text.strip() + + # Valor Aduanal Total + valor_aduanal = encabezado.find('ns2:valorAduanalTotal', namespaces) + if valor_aduanal is not None and valor_aduanal.text: + try: + resultado['valor_aduanal_total'] = float(valor_aduanal.text) + except (ValueError, TypeError): + pass + + # Valor Comercial Total + valor_comercial = encabezado.find('ns2:valorComercialTotal', namespaces) + if valor_comercial is not None and valor_comercial.text: + try: + resultado['valor_comercial_total'] = float(valor_comercial.text) + except (ValueError, TypeError): + pass + + # Fechas + fechas = pedimento_elem.findall('.//ns2:fechas', namespaces) + for fecha_elem in fechas: + fecha = fecha_elem.find('ns2:fecha', namespaces) + clave_fecha = fecha_elem.find('ns2:tipo/ns2:clave', namespaces) + + if fecha is not None and fecha.text and clave_fecha is not None and clave_fecha.text: + + fecha_texto = fecha.text.strip() + clave_fecha_texto = clave_fecha.text.strip() + + # Mapeo de claves según especificación + if clave_fecha_texto == '1': # Entrada + resultado['fecha_entrada'] = fecha_texto + elif clave_fecha_texto == '2': # Pago + resultado['fecha_pago'] = fecha_texto + elif clave_fecha_texto == '3': # Extracción + resultado['fecha_extraccion'] = fecha_texto + elif clave_fecha_texto == '5': # Presentación + resultado['fecha_presentacion'] = fecha_texto + elif clave_fecha_texto == '6': # Importación + resultado['fecha_importacion'] = fecha_texto + elif clave_fecha_texto == '7': # Original + resultado['fecha_original'] = fecha_texto + else: + resultado[f'fecha_clave_{clave_fecha_texto}'] = fecha_texto + + # Facturas (para COVEs) + facturas = pedimento_elem.findall('.//ns2:facturas', namespaces) + coves_encontrados = [] + for factura in facturas: + numero = factura.find('ns2:numero', namespaces) + if numero is not None and numero.text: + coves_encontrados.append(numero.text.strip()) + + if coves_encontrados: + resultado['coves_en_xml'] = coves_encontrados + + # E-Documents + identificadores = pedimento_elem.findall('.//ns2:identificadores/ns2:identificadores', namespaces) + edocs_encontrados = [] + for ident in identificadores: + clave = ident.find('claveIdentificador/descripcion', namespaces) + complemento = ident.find('complemento1', namespaces) + if clave is not None and clave.text and 'E_DOCUMENT' in clave.text: + if complemento is not None and complemento.text: + edocs_encontrados.append(complemento.text.strip()) + + if edocs_encontrados: + resultado['edocuments_en_xml'] = edocs_encontrados + + # Verificar si hay error en la respuesta + tiene_error = root.find('.//ns3:tieneError', namespaces) + if tiene_error is not None: + resultado['tiene_error'] = tiene_error.text.lower() == 'true' + + return resultado + + except ET.ParseError as e: + return {'error_parse': str(e)} + except Exception as e: + return {'error': str(e)} \ No newline at end of file diff --git a/api/customs/urls.py b/api/customs/urls.py index be61b50..68374cd 100644 --- a/api/customs/urls.py +++ b/api/customs/urls.py @@ -42,7 +42,24 @@ from .views_auditor import ( auditar_acuse_cove_pedimento_endpoint, auditar_edocument_pedimento_endpoint, auditar_acuse_pedimento_endpoint, - auditor_procesar_pedimentos_organizacion + auditar_procesamiento_remesa_pedimento_endpoint, + auditor_procesar_pedimentos_organizacion, + auditar_peticion_respuesta_pedimento_completo, + auditor_obtener_peticion_pedimento_vu, + auditor_obtener_respuesta_pedimento_vu, + auditor_obtener_peticion_remesa_vu, + auditor_obtener_respuesta_remesa_vu, + auditor_obtener_peticion_partidas_vu, + auditor_obtener_respuesta_partidas_vu, + auditor_obtener_peticion_acuse_vu, + auditor_obtener_respuesta_acuse_vu, + auditor_obtener_peticion_cove_vu, + auditor_obtener_respuesta_cove_vu, + auditor_obtener_peticion_acuse_cove_vu, + auditor_obtener_respuesta_acuse_cove_vu, + auditor_obtener_peticion_edocument_vu, + auditor_obtener_respuesta_edocument_vu, + auditar_pedimento_endpoint, ) urlpatterns = [ @@ -58,5 +75,24 @@ urlpatterns = [ path('auditor/auditar-acuse-cove/pedimento/', auditar_acuse_cove_pedimento_endpoint, name='auditar-acuse-cove-pedimento'), path('auditor/auditar-edocument/pedimento/', auditar_edocument_pedimento_endpoint, name='auditar-edocument-pedimento'), path('auditor/auditar-acuse/pedimento/', auditar_acuse_pedimento_endpoint, name='auditar-acuse-pedimento'), + path('auditor/auditar-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-remesa-pedimento'), + path('auditor/auditar-pedimento/', auditar_pedimento_endpoint, name='auditar-pedimento'), + path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'), + path('auditor/peticion-respuesta/pedimento-vu/', auditar_peticion_respuesta_pedimento_completo, name='peticion-respuesta-pedimento-vu'), + + path('auditor/obtener-peticion/pedimento-vu/', auditor_obtener_peticion_pedimento_vu, name='obtener-peticion-pedimento-vu'), + path('auditor/obtener-respuesta/pedimento-vu/', auditor_obtener_respuesta_pedimento_vu, name='obtener-respuesta-pedimento-vu'), + path('auditor/obtener-peticion/remesa-vu/', auditor_obtener_peticion_remesa_vu, name='obtener-peticion-remesa-vu'), + path('auditor/obtener-respuesta/remesa-vu/', auditor_obtener_respuesta_remesa_vu, name='obtener-respuesta-remesa-vu'), + path('auditor/obtener-peticion/partidas-vu/', auditor_obtener_peticion_partidas_vu, name='obtener-peticion-partidas-vu'), + path('auditor/obtener-respuesta/partidas-vu/', auditor_obtener_respuesta_partidas_vu, name='obtener-respuesta-partidas-vu'), + path('auditor/obtener-peticion/acuse-vu/', auditor_obtener_peticion_acuse_vu, name='obtener-peticion-acuse-vu'), + path('auditor/obtener-respuesta/acuse-vu/', auditor_obtener_respuesta_acuse_vu, name='obtener-respuesta-acuse-vu'), + path('auditor/obtener-peticion/cove-vu/', auditor_obtener_peticion_cove_vu, name='obtener-peticion-cove-vu'), + path('auditor/obtener-respuesta/cove-vu/', auditor_obtener_respuesta_cove_vu, name='obtener-respuesta-cove-vu'), + path('auditor/obtener-peticion/acuse-cove-vu/', auditor_obtener_peticion_acuse_cove_vu, name='obtener-peticion-acuse-cove-vu'), + path('auditor/obtener-respuesta/acuse-cove-vu/', auditor_obtener_respuesta_acuse_cove_vu, name='obtener-respuesta-acuse-cove-vu'), + path('auditor/obtener-peticion/edocument-vu/', auditor_obtener_peticion_edocument_vu, name='obtener-peticion-edocument-vu'), + path('auditor/obtener-respuesta/edocument-vu/', auditor_obtener_respuesta_edocument_vu, name='obtener-respuesta-edocument-vu'), ] \ No newline at end of file diff --git a/api/customs/views.py b/api/customs/views.py index 5125cc9..b0e21f7 100644 --- a/api/customs/views.py +++ b/api/customs/views.py @@ -57,6 +57,9 @@ try: except ImportError: RAR_SUPPORT = False +# Importar tarea de procesamiento de pedimento (Celery) +from api.customs.tasks.microservice import procesar_pedimento_completo_individual + def get_available_extractors(): """ Devuelve lista de extractores disponibles en orden de preferencia @@ -369,6 +372,25 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada ] } + @action(detail=True, methods=['post'], url_path='procesar-completo') + def procesar_completo(self, request, pk=None): + """ + Acción para disparar el procesamiento completo de un pedimento existente. + Dispara la tarea `procesar_pedimento_completo_individual` de forma asíncrona + y devuelve el `task_id`. + """ + pedimento = self.get_object() + try: + # Usar el nombre del servicio de Docker Compose en lugar de localhost + task = procesar_pedimento_completo_individual.delay(pedimento.id, pedimento.organizacion.id) + # Verificar si la respuesta fue exitosa + if task.id: + return Response({"status": "Recurso creado exitosamente en API", "task_id": task.id}, status=status.HTTP_202_ACCEPTED) + else: + return Response({"status": "Servicio API respondió con error", "task_id": 0}, status=status.HTTP_202_ACCEPTED) + except Exception as e: + return Response({"error": f"Error inesperado al llamar al servicio API: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @action(detail=False, methods=['post'], url_path='bulk-delete') def bulk_delete(self, request): """ diff --git a/api/customs/views_auditor.py b/api/customs/views_auditor.py index bef5302..1daf3c0 100644 --- a/api/customs/views_auditor.py +++ b/api/customs/views_auditor.py @@ -1,3 +1,4 @@ +import os from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -22,7 +23,9 @@ from .tasks.internal_services import auditar_pedimentos from .tasks.microservice_v2 import procesar_pedimentos_completos from api.customs.models import Pedimento from api.organization.models import Organizacion - +from api.record.models import Document +from .tasks.auditoria import auditar_pedimento_por_id +from .tasks.auditoria_xml import extraer_info_pedimento_xml @swagger_auto_schema( method='post', @@ -632,4 +635,1139 @@ def auditor_procesar_pedimentos_organizacion(request): }, status=status.HTTP_200_OK) ### Fin Procesamiento de pedimentos ### +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditar_peticion_respuesta_pedimento_completo(request): + """ + Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento. + """ + pedimento_id = request.data.get('pedimento_id') + vista_auditar = request.data.get('vista', 'desconocido') # 'completa' o 'resumen' + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + pedimento_app = pedimento.pedimento_app + tipo_documento_peticion = None + tipo_documento_respuesta = None + + if vista_auditar == 'pc': + tipo_documento_peticion = 13 + tipo_documento_respuesta = 14 + elif vista_auditar == 'rm': + tipo_documento_peticion = 15 + tipo_documento_respuesta = 16 + elif vista_auditar == 'pt': + tipo_documento_peticion = 17 + tipo_documento_respuesta = 18 + elif vista_auditar == 'cove': + tipo_documento_peticion = 19 + tipo_documento_respuesta = 20 + elif vista_auditar == 'edoc': + tipo_documento_peticion = 21 + tipo_documento_respuesta = 22 + elif vista_auditar == 'ac_cove': + tipo_documento_peticion = 23 + tipo_documento_respuesta = 24 + elif vista_auditar == 'ac': + tipo_documento_peticion = 25 + tipo_documento_respuesta = 26 + + if not tipo_documento_peticion and not tipo_documento_respuesta: + return Response( + {'error': 'Tipo de vista no reconocido para auditoría de pedimento'}, + status=status.HTTP_400_BAD_REQUEST + ) + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + #archivo__icontains= f"VU_PT_{pedimento_app}_REQUEST.xml", + document_type= tipo_documento_peticion, # Tipo de documento para petición de partidas + organizacion=pedimento.organizacion, + ) + + documentos_respuesta = Document.objects.filter( + pedimento=pedimento, + #archivo__icontains= f"VU_PT_{pedimento_app}_REQUEST.xml", + document_type= tipo_documento_respuesta, # Tipo de documento para respuesta de partidas + organizacion=pedimento.organizacion, + ) + + if not documentos_peticion and not documentos_respuesta: + return Response( + {'error': 'Registro de documentos de petición y respuesta de partidas no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + # Crear lista con todos los documentos encontrados + documentos_lista_peticiones = [] + for documento in documentos_peticion: + + nombre_archivo = os.path.basename(documento.archivo.name) + + documentos_lista_peticiones.append({ + 'id': str(documento.id), + 'archivo': documento.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documento.extension, + 'size': documento.size, + 'tipo': documento.document_type.descripcion if documento.document_type else 'Desconocido', + 'creado_en': documento.created_at, + 'actualizado_en': documento.updated_at + }) + + # Crear lista vacía para respuestas (por si se requiere en el futuro) + documentos_lista_respuestas = [] + for documento in documentos_respuesta: + + nombre_archivo = os.path.basename(documento.archivo.name) + + documentos_lista_respuestas.append({ + 'id': str(documento.id), + 'archivo': documento.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documento.extension, + 'size': documento.size, + 'tipo': documento.document_type.descripcion if documento.document_type else 'Desconocido', + 'creado_en': documento.created_at, + 'actualizado_en': documento.updated_at + }) + + # return Response({ + # 'id': pedimento.id, + # 'pedimento_app': pedimento_app, + # 'contribuyente': getattr(pedimento.contribuyente, 'rfc', None), + # 'organizacion': getattr(pedimento.organizacion, 'nombre', None), + # 'creado': pedimento.created_at + # }, status=status.HTTP_200_OK) + return Response({ + 'id': str(pedimento.id), + 'pedimento_id': str(pedimento.id), + 'pedimento': pedimento.pedimento, + 'pedimento_app': pedimento_app, + 'contribuyente': getattr(pedimento.contribuyente, 'rfc', None), + 'organizacion': getattr(pedimento.organizacion, 'nombre', None), + 'creado': pedimento.created_at, + 'total_documentos_peticiones': len(documentos_lista_peticiones), + 'total_documentos_respuestas': len(documentos_lista_respuestas), + 'documentos_peticiones': documentos_lista_peticiones, + 'documentos_respuestas': documentos_lista_respuestas + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_pedimento_vu(request): + """ + Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_PC_{pedimento_app}_REQUEST.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_pedimento_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a un pedimento. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_PC_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_remesa_vu(request): + """ + Backend endpoint para obtener las peticiones asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_RM_{pedimento_app}_REQUEST.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición de remesa no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_remesa_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_RM_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta de remesa no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_partidas_vu(request): + """ + Backend endpoint para obtener las peticiones asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + #archivo__icontains= f"VU_PT_{pedimento_app}_REQUEST.xml", + document_type= 17, # Tipo de documento para petición de partidas + organizacion=pedimento.organizacion, + ) + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición de partidas no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + # Crear lista con todos los documentos encontrados + documentos_lista = [] + + for documento in documentos_peticion: + + nombre_archivo = os.path.basename(documento.archivo.name) + + documentos_lista.append({ + 'id': str(documento.id), + 'archivo': documento.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documento.extension, + 'size': documento.size, + 'tipo': documento.document_type.descripcion if documento.document_type else 'Desconocido', + 'creado_en': documento.created_at, + 'actualizado_en': documento.updated_at + }) + + # nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + # return Response({ + # 'id': documentos_peticion.id, + # 'archivo': documentos_peticion.archivo.path, + # 'archivo_original': nombre_archivo, + # 'extension': documentos_peticion.extension, + # 'size': documentos_peticion.size, + # 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + # }, status=status.HTTP_200_OK) + return Response({ + 'pedimento_id': str(pedimento.id), + 'pedimento': pedimento.pedimento, + 'pedimento_app': pedimento.pedimento_app, + 'total_documentos': len(documentos_lista), + 'documentos': documentos_lista + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_partidas_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_PT_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta de partidas no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_acuse_vu(request): + """ + Backend endpoint para obtener las peticiones asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_AC_{pedimento_app}_REQUEST.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición de acuse no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_acuse_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_AC_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta de acuse no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_cove_vu(request): + """ + Backend endpoint para obtener las peticiones asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_COVE_{pedimento_app}_REQUEST.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición de cove no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_cove_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_COVE_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta de cove no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_acuse_cove_vu(request): + """ + Backend endpoint para obtener las peticiones asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_AC_COVE_{pedimento_app}_REQUEST.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición de acuse cove no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_acuse_cove_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_AC_COVE_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta de acuse cove no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_peticion_edocument_vu(request): + """ + Backend endpoint para obtener las peticiones asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_ED_{pedimento_app}_REQUEST.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de petición de e-document no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticament + + }, status=status.HTTP_200_OK) + +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditor_obtener_respuesta_edocument_vu(request): + """ + Backend endpoint para obtener las respuestas asociadas a una remesa. + """ + pedimento_id = request.data.get('pedimento_id') + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar permisos y existencia del pedimento + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + pedimento_app = pedimento.pedimento_app + + documentos_peticion = Document.objects.filter( + pedimento=pedimento, + archivo__icontains= f"VU_ED_{pedimento_app}_ERROR.xml", + organizacion=pedimento.organizacion, + ).first() + + if not documentos_peticion: + return Response( + {'error': 'Documento de respuesta de e-document no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + + nombre_archivo = os.path.basename(documentos_peticion.archivo.name) + return Response({ + 'id': documentos_peticion.id, + 'archivo': documentos_peticion.archivo.path, + 'archivo_original': nombre_archivo, + 'extension': documentos_peticion.extension, + 'size': documentos_peticion.size, + 'tipo': documentos_peticion.document_type.descripcion # O detectar automáticamente + + }, status=status.HTTP_200_OK) + + +@swagger_auto_schema( + method='post', + operation_description="Audita un pedimento específico verificando su XML y extrayendo información", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'pedimento_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID del pedimento') + }, + required=['pedimento_id'] + ), + responses={ + 200: openapi.Response('Auditoría completada'), + 400: openapi.Response('Error en los parámetros'), + 403: openapi.Response('No tiene permisos suficientes'), + 404: openapi.Response('Pedimento no encontrado') + } +) +@api_view(['POST']) +@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) +def auditar_pedimento_endpoint(request): + """ + Audita un pedimento específico verificando si existe su XML y extrayendo información. + """ + pedimento_id = request.data.get('pedimento_id') + + if not pedimento_id: + return Response( + {'error': 'Debe proporcionar pedimento_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + # Validar permisos y existencia del pedimento + pedimento = Pedimento.objects.get(id=pedimento_id) + user = request.user + + if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id): + return Response( + {'error': 'No tiene permisos para este pedimento'}, + status=status.HTTP_403_FORBIDDEN + ) + + # Buscar documentos XML del pedimento + documentos_xml = Document.objects.filter( + pedimento=pedimento, + archivo__endswith='.xml', + organizacion=pedimento.organizacion + ) + + if not documentos_xml.exists(): + return Response({ + 'pedimento_id': str(pedimento_id), + 'pedimento': pedimento.pedimento, + 'pedimento_app': pedimento.pedimento_app, + 'archivos_xml_encontrados': 0, + 'mensaje': 'No se encontraron archivos XML para este pedimento', + 'auditoria_completa': False + }, status=status.HTTP_200_OK) + + # Lista para almacenar información de cada XML + xmls_analizados = [] + informacion_extraida = [] + + for documento in documentos_xml: + try: + xml_info = { + 'documento_id': str(documento.id), + 'nombre_archivo': os.path.basename(documento.archivo.name), + 'tamanio': documento.size, + 'extension': documento.extension, + 'tipo_documento': documento.document_type.descripcion if documento.document_type else 'Desconocido' + } + + # Intentar extraer información del XML + try: + with open(documento.archivo.path, 'r', encoding='utf-8') as xml_file: + xml_content = xml_file.read() + + # Extraer información específica del XML + info_pedimento = extraer_info_pedimento_xml(xml_content) + + if info_pedimento: + xml_info['informacion_extraida'] = info_pedimento + informacion_extraida.append(info_pedimento) + + # Actualizar el pedimento con la información encontrada si es necesario + actualizar_info_pedimento(pedimento, info_pedimento) + + except Exception as e: + xml_info['error_lectura'] = str(e) + + xmls_analizados.append(xml_info) + + except Exception as e: + xmls_analizados.append({ + 'documento_id': str(documento.id), + 'nombre_archivo': os.path.basename(documento.archivo.name), + 'error': f'Error procesando archivo: {str(e)}' + }) + + # Ejecutar la tarea de auditoría completa + task = auditar_pedimento_por_id.delay(pedimento_id) + + response_data = { + 'pedimento_id': str(pedimento_id), + 'pedimento': pedimento.pedimento, + 'pedimento_app': pedimento.pedimento_app, + 'archivos_xml_encontrados': len(xmls_analizados), + 'xmls_analizados': xmls_analizados, + 'informacion_extraida': informacion_extraida, + 'auditoria_completa': True, + 'task_id': task.id, + 'mensaje': f'Auditoría completada para el pedimento {pedimento.pedimento}' + } + + return Response(response_data, status=status.HTTP_200_OK) + + except Pedimento.DoesNotExist: + return Response( + {'error': 'Pedimento no encontrado'}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {'error': f'Error en la auditoría: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +def actualizar_info_pedimento(pedimento, info_xml): + """ + Actualiza la información del pedimento con los datos extraídos del XML. + """ + try: + actualizado = False + + # Actualizar información del pedimento si está en el XML y no está ya llena + if 'numero_operacion' in info_xml and not pedimento.numero_operacion: + pedimento.numero_operacion = info_xml['numero_operacion'] + actualizado = True + + # Número de partidas + if 'numero_partidas' in info_xml and not pedimento.numero_partidas: + pedimento.numero_partidas = info_xml['numero_partidas'] + actualizado = True + + # Clave del pedimento + if 'clave_pedimento' in info_xml and not pedimento.clave_pedimento: + pedimento.clave_pedimento = info_xml['clave_pedimento'] + actualizado = True + + # Aduana (patente) + if 'aduana_clave' in info_xml and not pedimento.aduana: + pedimento.aduana = info_xml['aduana_clave'] + actualizado = True + + # RFC Agente Aduanal + if 'rfc_agente_aduanal' in info_xml and not pedimento.agente_aduanal: + pedimento.agente_aduanal = info_xml['rfc_agente_aduanal'] + actualizado = True + + # CURP Apoderado + if 'curp_apoderado' in info_xml and not pedimento.curp_apoderado: + pedimento.curp_apoderado = info_xml['curp_apoderado'] + actualizado = True + + # Fecha de pago + if 'fecha_pago' in info_xml and not pedimento.fecha_pago: + try: + # Convertir formato de fecha (ej: "2024-02-15-06:00") + fecha_str = info_xml['fecha_pago'] + # Extraer solo la parte de la fecha (antes del primer '-') + fecha_parts = fecha_str.split('-') + if len(fecha_parts) >= 3: + fecha_simple = f"{fecha_parts[0]}-{fecha_parts[1]}-{fecha_parts[2]}" + from datetime import datetime + fecha_obj = datetime.strptime(fecha_simple, '%Y-%m-%d').date() + pedimento.fecha_pago = fecha_obj + actualizado = True + except (ValueError, TypeError, IndexError): + pass + + # Importe total (valor en dólares) + if 'valor_dolares' in info_xml and not pedimento.importe_total: + try: + pedimento.importe_total = float(info_xml['valor_dolares']) + actualizado = True + except (ValueError, TypeError): + pass + + if 'contribuyente_rfc' in info_xml and not pedimento.contribuyente: + try: + # Buscar o crear el importador + from api.customs.models import Importador + importador, created = Importador.objects.get_or_create( + rfc=info_xml['contribuyente_rfc'], + organizacion=pedimento.organizacion, + defaults={'nombre': info_xml.get('contribuyente_nombre', '')} + ) + pedimento.contribuyente = importador + actualizado = True + except Exception: + pass + + if 'tipo_operacion' in info_xml and not pedimento.tipo_operacion: + try: + from api.customs.models import TipoOperacion + tipo_op_obj, created = TipoOperacion.objects.get_or_create( + tipo=info_xml['tipo_operacion'], + defaults={'descripcion': info_xml['tipo_operacion_descripcion'][:100]} # Limitar a 100 caracteres + ) + pedimento.tipo_operacion = tipo_op_obj + actualizado = True + except Exception: + pass + + if actualizado: + pedimento.save() + return True + + return False + + except Exception: + return False \ No newline at end of file diff --git a/api/record/urls.py b/api/record/urls.py index 1ea522d..524508c 100644 --- a/api/record/urls.py +++ b/api/record/urls.py @@ -11,7 +11,8 @@ from .views import (DocumentViewSet , DocumentTypeView , ExpedienteZipDownloadView , MultiPedimentoZipDownloadView - , PedimentoDocumentViewSet) + , PedimentoDocumentViewSet + , TriggerPedimentoCompletoView) # Create a router and register your viewsets with it @@ -35,5 +36,6 @@ urlpatterns = [ path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'), path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'), path('pedimento-documents/', PedimentoDocumentViewSet.as_view({'get': 'list'}), name='pedimento-document-list'), + path('microservice/pedimento-completo/', TriggerPedimentoCompletoView.as_view(), name='trigger-pedimento-completo'), path('', include(router.urls)), ] \ No newline at end of file diff --git a/api/record/views.py b/api/record/views.py index 888aee2..d312509 100644 --- a/api/record/views.py +++ b/api/record/views.py @@ -14,6 +14,7 @@ from rest_framework.exceptions import ValidationError from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer from .models import Document, Fuente, DocumentType from ..customs.models import Pedimento +from ..vucem.models import CredencialesImportador from api.organization.models import UsoAlmacenamiento from io import BytesIO import zipfile @@ -35,9 +36,91 @@ logger = logging.getLogger(__name__) import os from django.core.files.storage import default_storage +from django.conf import settings +import requests +import re from mixins.filtrado_organizacion import DocumentosFiltradosMixin +# Configuración de patrones y mapeos (definirlo fuera del try para reutilización) +DOCUMENT_PATTERNS = { + 'REQUEST': { + 'VU_PC': (r".*VU_PC.*REQUEST\.xml$", 13, "Request Pedimento Completo VU"), + 'VU_ED': (r".*VU_ED.*REQUEST\.xml$", 21, "Request E-Document VU"), + 'VU_PT': (r".*VU_PT.*REQUEST\.xml$", 17, "Request Partidas VU"), + 'VU_AC_COVE': (r".*VU_AC_COVE.*REQUEST\.xml$", 23, "Request Acuses COVES VU"), + 'VU_COVE': (r".*VU_COVE.*REQUEST\.xml$", 19, "Request COVES VU"), + 'VU_RM': (r".*VU_RM.*REQUEST\.xml$", 15, "Request Remesas VU"), + 'VU_AC': (r".*VU_AC.*REQUEST\.xml$", 25, "Request Acuses VU"), + }, + 'ERROR': { + 'VU_PC': (r".*VU_PC.*ERROR\.xml$", 14, "Error Pedimento Completo VU"), + 'VU_ED': (r".*VU_ED.*ERROR\.xml$", 22, "Error E-Document VU"), + 'VU_PT': (r".*VU_PT.*ERROR\.xml$", 18, "Error Partidas VU"), + 'VU_AC_COVE': (r".*VU_AC_COVE.*ERROR\.xml$", 24, "Error Acuses COVES VU"), + 'VU_COVE': (r".*VU_COVE.*ERROR\.xml$", 20, "Error COVES VU"), + 'VU_RM': (r".*VU_RM.*ERROR\.xml$", 16, "Error Remesas VU"), + 'VU_AC': (r".*VU_AC.*ERROR\.xml$", 26, "Error Acuses VU"), + } +} + +def eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id): + + """Elimina documentos existentes con el mismo nombre base""" + documentos_existentes = Document.objects.filter( + archivo__icontains=nombre_sin_extension, + organizacion=organizacion, + pedimento_id=pedimento_id + ) + + if not documentos_existentes.exists(): + return + + for doc_existente in documentos_existentes: + # Eliminar archivo físico si existe + if doc_existente.archivo and os.path.exists(doc_existente.archivo.path): + try: + os.remove(doc_existente.archivo.path) + except Exception as e: + logger.error(f"Error al eliminar archivo físico: {e}") + + # Eliminar registros de la base de datos + documentos_existentes.delete() + +def obtener_tipo_documento_por_patron(nombre_archivo, organizacion, pedimento_id): + """Determina el tipo de documento basado en patrones de nombre""" + nombre_sin_extension = nombre_archivo.rsplit('.', 1)[0] + + # Verificar patrones REQUEST + for doc_key, (patron, type_id, descripcion) in DOCUMENT_PATTERNS['REQUEST'].items(): + if re.search(patron, nombre_archivo, re.IGNORECASE): + try: + # Eliminar documentos existentes + eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id) + + # Obtener tipo de documento + return DocumentType.objects.get(id=type_id) + except DocumentType.DoesNotExist: + raise ValidationError({ + "error": f"El tipo de documento '{descripcion}' no existe. Por favor, créelo primero." + }) + + # Verificar patrones ERROR (si es necesario procesarlos) + for doc_key, (patron, type_id, descripcion) in DOCUMENT_PATTERNS['ERROR'].items(): + if re.search(patron, nombre_archivo, re.IGNORECASE): + try: + # Eliminar documentos existentes + eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id) + + # Obtener tipo de documento + return DocumentType.objects.get(id=type_id) + except DocumentType.DoesNotExist: + raise ValidationError({ + "error": f"El tipo de documento '{descripcion}' no existe. Por favor, créelo primero." + }) + + return None + class CustomPagination(PageNumberPagination): """ @@ -144,12 +227,45 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin): "codigo": "storage_limit_exceeded" }, code=status.HTTP_400_BAD_REQUEST) - # Guardar documento y actualizar espacio atómicamente - documento = serializer.save( - organizacion=organizacion, - size=archivo.size, - extension=archivo.name.split('.')[-1].lower() - ) + + try: + + pedimento_id = serializer.validated_data.get('pedimento').id if serializer.validated_data.get('pedimento') else None + document_type = serializer.validated_data.get('document_type') + + # Determinar el tipo de documento basado en el nombre del archivo + detected_type = obtener_tipo_documento_por_patron(archivo.name, organizacion, pedimento_id) + + if detected_type: + document_type = detected_type + else: + # Lógica para archivos que no coinciden con los patrones conocidos + logger.warning(f"No se encontró patrón para archivo: {archivo.name}") + # Puedes mantener el document_type original o manejarlo de otra forma + + except ValidationError as ve: + raise ve + except Exception as e: + # Como fallback, intentar obtener cualquier DocumentType existente + logger.error(f"Error al determinar el tipo de documento basado en el nombre del archivo: {e}") + + try: + + # Guardar documento y actualizar espacio atómicamente + documento = serializer.save( + document_type=document_type, + organizacion=organizacion, + size=archivo.size, + extension=archivo.name.split('.')[-1].lower() + ) + + except Exception as e: + # Guardar documento y actualizar espacio atómicamente + documento = serializer.save( + organizacion=organizacion, + size=archivo.size, + extension=archivo.name.split('.')[-1].lower() + ) uso.espacio_utilizado = nuevo_espacio_utilizado uso.save() @@ -833,7 +949,114 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin): queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero) return queryset +class TriggerPedimentoCompletoView(APIView): + """ + Endpoint interno para disparar la descarga de pedimento completo + en el microservicio FastAPI. Reenvía el payload tal cual y devuelve + la respuesta del microservicio (normalmente un `task_id`). + """ + # permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )] + my_tags = ['Microservice - Pedimento Completo'] + + def post(self, request): + + if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): + return Response({"error": "Usuario no autenticado o sin organización"}, status=401) + + # Validación mínima + # if not payload.get('credencial') or not payload.get('pedimento_id'): + # return Response({"error": "Se requieren 'credencial' y 'pedimento'"}, status=status.HTTP_400_BAD_REQUEST) + + if not request.data.get('pedimento_id'): + return Response({"error": "Se requieren 'credencial' y 'pedimento'"}, status=status.HTTP_400_BAD_REQUEST) + + pedimento_id = request.data.get('pedimento_id') + # Verificar que el pedimento existe y pertenece a la organización del usuario + try: + pedimento = Pedimento.objects.get(id=pedimento_id) + + if not pedimento.contribuyente: + return Response({"error": "El pedimento no tiene un contribuyente asociado"}, status=status.HTTP_400_BAD_REQUEST) + + contribuyente_rfc = pedimento.contribuyente.rfc + + payload = { + "pedimento": { + "id": str(pedimento.id), + "pedimento": pedimento.pedimento, + "pedimento_app": pedimento.pedimento_app, + "aduana": pedimento.aduana, + "patente": pedimento.patente, + "contribuyente": contribuyente_rfc, + "organizacion": str(pedimento.organizacion.id) + }, + "credencial": { + "id": "", + "user": "", + "password": "", + "efirma": "", + "key": "", + "cer": "", + "is_active": False, + "organizacion": "" + } + } + except Pedimento.DoesNotExist: + return Response({"error": "Pedimento no encontrado"}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({"error": f"Error al buscar pedimento: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + try: + credenciales = CredencialesImportador.objects.get(rfc=contribuyente_rfc) + vucem = credenciales.vucem + + # Obtener las rutas de los archivos, no los objetos FieldFile + key_path = vucem.key.path if vucem.key else "" + cer_path = vucem.cer.path if vucem.cer else "" + + payload['credencial'] = { + "id": str(credenciales.id), + "user": vucem.usuario if vucem.usuario else "", + "password": vucem.password if vucem.password else "", + "efirma": vucem.efirma if vucem.efirma else "", + "key": key_path, + "cer": cer_path, + "is_active": vucem.is_active if vucem.is_active else False, + "organizacion": str(credenciales.organizacion.id) if credenciales.organizacion else "" + } + except CredencialesImportador.DoesNotExist: + return Response({"error": "No se encontró credencial VUCEM para la organización del pedimento"}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({"error": f"Error al buscar credencial VUCEM: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + try: + # Obtener la URL desde las variables de entorno + api_url = os.getenv('SERVICE_API_URL_V2') + logger.info(f"Usando MICROSERVICE_BASE_URL: {api_url}") + endpoint = f"{api_url.rstrip('/')}/services/auditar_pedimento_completo" + # endpoint = "http://localhost:8001/api/v2/services/auditar_pedimento_completo" + except requests.exceptions.RequestException as e: + logger.error(f"Error obteniendo MICROSERVICE_BASE_URL: {e}") + return Response({"error": "Error obteniendo la URL del microservicio", "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + except Exception as e: + logger.error(f"Error inesperado obteniendo MICROSERVICE_BASE_URL: {e}") + return Response({"error": "Error inesperado obteniendo la URL del microservicio", "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + try: + resp = requests.post(endpoint, json=payload, timeout=30) + except requests.exceptions.RequestException as e: + logger.error(f"Error comunicándose con microservice: {e}") + return Response({"error": "No se pudo conectar con el microservicio", "detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY) + + try: + content = resp.json() + except ValueError: + content = {"detail": resp.text} + + return Response(content, status=resp.status_code) + \ No newline at end of file