From 942847680a34fcc8b5a9bb7ede1aadcb911cc579 Mon Sep 17 00:00:00 2001 From: Luis Date: Tue, 16 Dec 2025 07:29:54 -0700 Subject: [PATCH] Fix: Se crea nuevo endpoint para subir documentos a expediente electronico. --- api/customs/views.py | 553 +++++++++++++++++++++++++++++++++++++- api/record/serializers.py | 11 +- 2 files changed, 558 insertions(+), 6 deletions(-) diff --git a/api/customs/views.py b/api/customs/views.py index 4bddd38..85f478e 100644 --- a/api/customs/views.py +++ b/api/customs/views.py @@ -23,7 +23,7 @@ from api.customs.models import ( EDocument, Cove, Importador, - Partida + Partida, ) from api.customs.serializers import ( PedimentoSerializer, @@ -44,11 +44,11 @@ import zipfile import tempfile import shutil import subprocess -from datetime import datetime +from datetime import date, datetime from django.core.files.base import ContentFile from django.db import transaction from rest_framework.parsers import MultiPartParser, FormParser -from api.record.models import Document, DocumentType +from api.record.models import Document, DocumentType, Fuente # Importar rarfile de manera opcional try: @@ -281,7 +281,16 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada ordering = ['-created_at'] # Orden descendente por fecha de creación por defecto def get_queryset(self): - return self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador + + queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador + + # pedimento_app_filter = self.request.GET.get('pedimento_app', None) + + # if pedimento_app_filter: + # print(f"Filtro por pedimento_app: {pedimento_app_filter}") + # queryset = queryset.filter(pedimento_app__icontains=pedimento_app_filter) + + return queryset def perform_create(self, serializer): """ @@ -883,6 +892,542 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada return Response(response_data, status=response_status) + @action(detail=False, methods=['post'], url_path='bulk-create-pedimento_desk', parser_classes=[MultiPartParser, FormParser]) + def bulk_create_pedimento_desk(self, request): + """ + Endpoint para crear múltiples pedimentos desde EFC APP Desk. + + FormData esperado: + - contribuyente: string (nombre del contribuyente) + - archivos: files (pueden ser múltiples archivos: zip, rar o individuales) + + Nomenclatura esperada de archivos: anio-aduana-patente-pedimento + - anio: 2 dígitos (ej: 24) + - aduana: 2 o 3 dígitos (ej: 01, 123) + - patente: 4 dígitos (ej: 3420) + - pedimento: 7 dígitos (ej: 1234567) + + Ejemplo: 24-01-3420-1234567 + + Nota: Cada archivo ZIP/RAR se procesa independientemente en su propio subdirectorio. + + Respuesta exitosa: + { + "message": "Pedimentos creados exitosamente", + "created_count": 5, + "created_pedimentos": [...], + "documents_created": 15, + "processed_files": 3, + "summary": "Procesados 3 archivo(s): 5 pedimento(s) creado(s), 15 documento(s) asociado(s)", + "failed_files": [], + "errors": [] + } + """ + print(request.data) + # Validar datos requeridos + contribuyente = request.data.get('contribuyente') + fecha_pago_input = request.data.get('fecha_pago') + clave_pedimento_input = request.data.get('clave_pedimento') + patente_input = request.data.get('patente') + tipo_operacion_input = request.data.get('tipo_operacion') + aduana_input = request.data.get('aduana') + contribuyente_input = request.data.get('contribuyente') + curp_apoderado_input = request.data.get('curp_apoderado') + partidas_input = request.data.get('partidas') + fuente_archivos = request.data.get('partidas') + + archivos = request.FILES.getlist('archivos') + + # if not contribuyente: + # return Response( + # {"error": "Se requiere el campo 'contribuyente'"}, + # status=status.HTTP_400_BAD_REQUEST + # ) + + if not archivos: + return Response( + { + "tieneError": True, + "error": "Se requiere al menos un archivo" + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validar organización del usuario + if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): + return Response( + { + "tieneError": True, + "error": "Usuario no autenticado o sin organización" + }, + status=status.HTTP_400_BAD_REQUEST + ) + + organizacion = request.user.organizacion + + # Regex para validar nomenclatura: anio-aduana-patente-pedimento + nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$') + nomenclatura_pattern_sin_anio = re.compile(r'^(\d{2,3})-(\d{4})-(\d{7})$') + + created_pedimentos = [] + failed_files = [] + errors = [] + documents_created = 0 + temp_dir = None + + # Obtener DocumentType ANTES de la transacción atómica + print("Intentando obtener o crear DocumentType...") + try: + # Primero intentar obtener si ya existe + try: + document_type = DocumentType.objects.get(nombre="Pedimento") + print(f"DocumentType obtenido existente: {document_type.nombre} (ID: {document_type.id})") + except DocumentType.DoesNotExist: + # Si no existe, crear uno nuevo + document_type = DocumentType.objects.create( + nombre="Pedimento", + descripcion="Documento de pedimento" + ) + print(f"DocumentType creado nuevo: {document_type.nombre} (ID: {document_type.id})") + + except Exception as e: + print(f"Error al obtener/crear DocumentType: {str(e)}") + # Como fallback, intentar obtener cualquier DocumentType existente + try: + document_type = DocumentType.objects.first() + if document_type: + print(f"Usando DocumentType existente como fallback: {document_type.nombre} (ID: {document_type.id})") + else: + print("No hay DocumentType disponible") + return Response( + {"error": "No se pudo configurar el tipo de documento y no hay tipos existentes"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + except Exception as fallback_error: + print(f"Error en fallback: {str(fallback_error)}") + return Response( + { + "tieneError": True, + "error": f"Error crítico al configurar tipo de documento: {str(e)}" + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + try: + print("Iniciando transacción atómica...") + with transaction.atomic(): + # Crear directorio temporal + temp_dir = tempfile.mkdtemp() + print(f"Directorio temporal creado: {temp_dir}") + + # Procesar cada archivo enviado + for idx, archivo in enumerate(archivos): + archivo_name = archivo.name.lower() + print(f"Procesando archivo {idx + 1}/{len(archivos)}: {archivo_name}") + + # Crear subdirectorio para cada archivo usando el nombre del archivo sin extensión + archivo_name_sin_extension = os.path.splitext(archivo.name)[0] + sub_dir = os.path.join(temp_dir, archivo_name_sin_extension) + os.makedirs(sub_dir, exist_ok=True) + print(f"Subdirectorio creado: {sub_dir}") + + if archivo_name.endswith('.zip'): + # Manejar archivo ZIP + print("Es un archivo ZIP") + try: + with zipfile.ZipFile(archivo, 'r') as zip_ref: + zip_ref.extractall(sub_dir) + print("Archivo ZIP extraído exitosamente") + except zipfile.BadZipFile as e: + return Response( + { + "tieneError": True, + "error": f"Archivo ZIP corrupto o inválido: {archivo.name} - {str(e)}" + }, + status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + return Response( + { + "tieneError": True, + "error": f"Error al extraer ZIP {archivo.name}: {str(e)}" + }, + status=status.HTTP_400_BAD_REQUEST + ) + elif archivo_name.endswith('.rar'): + # Manejar archivo RAR: guardar el archivo en disco y usar helper con fallbacks + # Guardar el archivo subido en un path temporal dentro del sub_dir + archivo_temp_path = os.path.join(sub_dir, archivo.name) + with open(archivo_temp_path, 'wb') as f: + for chunk in archivo.chunks(): + f.write(chunk) + try: + extract_rar_to_dir(archivo_temp_path, sub_dir) + print(f"Archivo RAR {archivo.name} extraído en {sub_dir}") + except Exception as e: + error_msg = str(e) + help_msg = "Instale 'unrar' o 'p7zip' (7z) y asegúrese de que estén en PATH, o instale y configure 'rarfile' con un backend." + return Response( + { + "tieneError": True, + "error": f"Error al extraer archivo RAR {archivo.name}: {error_msg}. {help_msg}" + }, + status=status.HTTP_400_BAD_REQUEST + ) + else: + # Asumir que es un archivo individual + # Crear el archivo en el subdirectorio + archivo_path = os.path.join(sub_dir, archivo.name) + with open(archivo_path, 'wb') as f: + for chunk in archivo.chunks(): + f.write(chunk) + print(f"Archivo individual {archivo.name} guardado en sub_dir:", archivo_path) + + # Recorrer todos los archivos extraídos o el directorio + print("Iniciando recorrido de archivos...") + for root, dirs, files in os.walk(temp_dir): + print(f"Revisando directorio: {root}") + print(f"Archivos encontrados: {files}") + + for file_name in files: + print(f"Procesando archivo: {file_name}") + file_path = os.path.join(root, file_name) + + # Obtener la ruta relativa para determinar la estructura de carpetas + relative_path = os.path.relpath(file_path, temp_dir) + print(f"Ruta relativa: {relative_path}") + + # Determinar si el archivo está en una carpeta que sigue la nomenclatura + folder_name = None + if os.path.dirname(relative_path): + # El archivo está dentro de una carpeta + folder_parts = relative_path.split(os.sep) + folder_name = folder_parts[0] # Primera carpeta (nombre del archivo ZIP/RAR sin extensión) + else: + # El archivo está en la raíz, usar el nombre del archivo sin extensión + folder_name = os.path.splitext(file_name)[0] + + print(f"Folder name para validación: {folder_name}") + + # Validar nomenclatura + match = nomenclatura_pattern.match(folder_name) + match_sin_anio = nomenclatura_pattern_sin_anio.match(folder_name) + + if not match and not match_sin_anio: + print(f"Nomenclatura inválida: {folder_name}") + # Determinar el archivo original basado en el subdirectorio + archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') + failed_files.append({ + "file": relative_path, + "archivo_original": archivo_original, + "error": f"Nomenclatura inválida: {folder_name}. Esperado: anio-aduana-patente-pedimento" + }) + continue + + if match: + + print(f"Nomenclatura válida: {folder_name}") + anio, aduana, patente, pedimento_num = match.groups() + print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") + # Formato original: anio-aduana-patente-pedimento + # Crear fecha_pago basada en el año + try: + # Convertir año de 2 dígitos a 4 dígitos + anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio) + fecha_pago = datetime(anio_completo, 1, 1).date() + print(f"Fecha de pago calculada: {fecha_pago}") + except ValueError: + archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') + failed_files.append({ + "file": relative_path, + "archivo_original": archivo_original, + "error": f"Año inválido: {anio}" + }) + continue + + elif match_sin_anio: + + print(f"Nomenclatura válida sin año: {folder_name}") + + # Formato sin año: aduana-patente-pedimento + aduana, patente, pedimento_num = match_sin_anio.groups() + print(f"Extraído - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") + + # Obtener el primer dígito del pedimento + primer_digito_pedimento = int(pedimento_num[0]) if pedimento_num else 0 + + # Usar año actual para fecha_pago y ajustar según el dígito del pedimento + año_actual = datetime.now().year + + # Crear año con el dígito del pedimento (reemplazando el último dígito) + año_con_digito = int(str(año_actual)[:-1] + str(primer_digito_pedimento)) + + # Aplicar lógica de comparación + if año_con_digito <= año_actual: + # Si el año con dígito es menor o igual al año actual + año_final = año_con_digito + else: + # Si el año con dígito es mayor al año actual, restar 10 + año_final = año_con_digito - 10 + + # Tomar los últimos 2 dígitos del año final + anio = año_final % 100 + + # Crear fecha de pago (primer día del año) + fecha_pago = datetime(año_final , 1, 1).date() + + print(f"Fecha de pago (año actual) calculada: {fecha_pago}") + + # Generar pedimento_app + pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}" + print(f"Pedimento_app generado: {pedimento_app}") + print(f"Buscando pedimento existente con pedimento_app: {pedimento_app} y organización ID: {organizacion.id}") + # Verificar si el pedimento ya existe + existing_pedimento = Pedimento.objects.filter( + pedimento=int(pedimento_num), + # pedimento_app=pedimento_app, + organizacion=organizacion + ).first() + + print(f"Pedimento existente: {existing_pedimento is not None}") + + if not existing_pedimento: + print("📝 Pedimento no existe, creando nuevo...") + # Crear nuevo pedimento + try: + print("🔄 Iniciando creación de pedimento...") + importador = None + + if contribuyente: + # Obtener o crear el importador + print(f"🏢 Buscando/creando importador con RFC: {contribuyente}") + importador, created = Importador.objects.get_or_create( + rfc=contribuyente, + defaults={ + 'nombre': f"Importador {contribuyente}", + 'organizacion': organizacion + } + ) + if created: + print(f"✅ Importador creado: {importador.rfc} - {importador.nombre}") + else: + print(f"♻️ Importador existente: {importador.rfc} - {importador.nombre}") + + if tipo_operacion_input: + tipo_operacion_input = TipoOperacion.objects.get(id=tipo_operacion_input) + + + pedimento = Pedimento.objects.create( + organizacion=organizacion, + contribuyente=importador if importador else None, + pedimento=int(pedimento_num), + aduana=int(aduana), + patente=int(patente), + fecha_pago=fecha_pago_input if fecha_pago_input else fecha_pago, + curp_apoderado=curp_apoderado_input if curp_apoderado_input else "", + numero_partidas=partidas_input if partidas_input else 0, + tipo_operacion=tipo_operacion_input if tipo_operacion_input else None, + pedimento_app=pedimento_app, + agente_aduanal=f"Agente {patente}", # Valor por defecto + clave_pedimento=clave_pedimento_input if clave_pedimento_input else "A1" # Valor por defecto + ) + + print(f"✅ Pedimento creado exitosamente: ID {pedimento.id}, pedimento_app: {pedimento_app}") + + created_pedimentos.append({ + "id": str(pedimento.id), + "pedimento_app": pedimento_app, + "contribuyente": getattr(importador, 'rfc', None), + "contribuyente_nombre": getattr(importador, 'nombre', None) + }) + + except Exception as e: + print(f"❌ Error al crear pedimento: {str(e)}") + archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') + failed_files.append({ + "file": relative_path, + "archivo_original": archivo_original, + "error": f"Error al crear pedimento: {str(e)}" + }) + continue + else: + print(f"♻️ Usando pedimento existente: ID {existing_pedimento.id}") + # Usar pedimento existente + # # Actualizar Importador + if contribuyente: + importador, created = Importador.objects.get_or_create( + rfc=contribuyente, + defaults={ + 'nombre': f"Importador {contribuyente}", + 'organizacion': organizacion + } + ) + + importador_db = existing_pedimento.contribuyente + if importador_db: + if importador_db != importador: + existing_pedimento.contribuyente = importador + else: + existing_pedimento.contribuyente = importador + + existing_pedimento.save() + + # Actualizar Tipo Operacion + if tipo_operacion_input: + tipo_operacion_input = TipoOperacion.objects.get(id=tipo_operacion_input) + if tipo_operacion_input: + tipo_operacion_db = existing_pedimento.tipo_operacion + if not tipo_operacion_db: + existing_pedimento.tipo_operacion = tipo_operacion_input + existing_pedimento.save() + + # Actualizar fecha de pago solo cuando aun no esta actualizado + if fecha_pago_input: + fecha_db = existing_pedimento.fecha_pago + + # Verificar si hay fecha en BD + if fecha_db: + # Asegurar que trabajamos con date + if isinstance(fecha_db, datetime): + fecha_db = fecha_db.date() + + # Si la fecha existe y es 1 de enero, actualizar + if fecha_db.month == 1 and fecha_db.day == 1: + # Actualizar Fecha + existing_pedimento.fecha_pago = fecha_pago_input + existing_pedimento.save() + else: + existing_pedimento.fecha_pago = fecha_pago_input + existing_pedimento.save() + + + if clave_pedimento_input: + clavePedimento = existing_pedimento.clave_pedimento + + if not clavePedimento: + existing_pedimento.clave_pedimento = clave_pedimento_input + existing_pedimento.save() + + if curp_apoderado_input: + curp = existing_pedimento.curp_apoderado + if not curp: + existing_pedimento.curp_apoderado = curp_apoderado_input + existing_pedimento.save() + + if partidas_input: + numPartidas = existing_pedimento.numero_partidas + if not numPartidas: + existing_pedimento.numero_partidas = partidas_input + existing_pedimento.save() + elif numPartidas <= 0: + existing_pedimento.numero_partidas = partidas_input + existing_pedimento.save() + + pedimento = existing_pedimento + + print(f"🔄 Iniciando creación de documento para pedimento ID: {pedimento.id}") + # Crear documento asociado al pedimento + try: + print("📖 Leyendo archivo desde directorio temporal...") + # Leer el archivo desde el directorio temporal + with open(file_path, 'rb') as f: + file_content = f.read() + + print(f"📄 Archivo leído: {len(file_content)} bytes") + # Crear ContentFile que Django puede manejar correctamente + django_file = ContentFile(file_content, name=file_name) + + # # Verificar si el documento ya existe para este pedimento y archivo + # print("🔍 Verificando existencia previa del documento...") + + # # Reemplazar múltiples caracteres + # normalized_file_name = file_name.replace(" ", "_") + + # file_name_without_extension = normalized_file_name.rsplit('.', 1)[0] + # extension_file = os.path.splitext(normalized_file_name)[1].lower().lstrip('.') + + # existing_document = Document.objects.filter( + # pedimento_id=pedimento.id, + # archivo__contains=file_name_without_extension, + # extension=extension_file + # ).first() + + # if existing_document: + # print(f"Documento existente encontrado, omitiendo creación: ID {existing_document.id}") + # continue + + # try: + # fuente = Fuente.objects.get(nombre="APP-EFC") + # except Fuente.DoesNotExist: + # fuente = Fuente.objects.create( + # nombre="APP-EFC", + # descripcion='Transmitido por la app de escritorio' + # ) + + fuente, created = Fuente.objects.get_or_create( + nombre="APP-EFC", + descripcion='Transmitido por la app de escritorio' + ) + + print(f"Creando documento para archivo: {file_name}") + # Crear documento - Django automáticamente guardará el archivo en media/documents/ + document = Document.objects.create( + organizacion=organizacion, + pedimento_id=pedimento.id, + document_type=document_type, + fuente_id=fuente.id, + archivo=django_file, + size=len(file_content), + extension=os.path.splitext(file_name)[1].lower().lstrip('.') + ) + print(f"Documento creado exitosamente: {document.id}") + + documents_created += 1 + print(f"📊 Total documentos creados hasta ahora: {documents_created}") + + except Exception as e: + print(f"❌ Error al crear documento: {str(e)}") + archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') + failed_files.append({ + "file": relative_path, + "archivo_original": archivo_original, + "error": f"Error al crear documento: {str(e)}" + }) + continue + + print(f"🏁 Procesamiento completado. Archivos procesados en este directorio.") + except Exception as e: + return Response( + { "tieneError": True, + "error": f"Error durante el procesamiento: {str(e)}" + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + finally: + # Limpiar directorio temporal + if temp_dir and os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + # Preparar respuesta + response_data = { + "tieneError": False, + "failed_files": failed_files, + "processed_files": len(archivos), + } + + if failed_files: + response_data["tieneError"] = True + response_data.update({ + "message": "Procesamiento completado con algunos errores", + "errors": [item["error"] for item in failed_files] + }) + response_status = status.HTTP_207_MULTI_STATUS + else: + response_data["message"] = "Pedimentos creados exitosamente" + response_status = status.HTTP_201_CREATED + + return Response(response_data, status=response_status) + my_tags = ['Pedimentos'] diff --git a/api/record/serializers.py b/api/record/serializers.py index 3a67892..cb8738a 100644 --- a/api/record/serializers.py +++ b/api/record/serializers.py @@ -9,10 +9,11 @@ from api.customs.models import Pedimento class DocumentSerializer(serializers.ModelSerializer): pedimento_numero = serializers.SerializerMethodField(read_only=True) pedimento = serializers.PrimaryKeyRelatedField(queryset=Pedimento.objects.all()) - + fuente_nombre = serializers.SerializerMethodField() + fuente = serializers.PrimaryKeyRelatedField(queryset=Fuente.objects.all()) class Meta: model = Document - fields = ('id', 'organizacion', 'pedimento', 'pedimento_numero', 'archivo', 'document_type', 'size', 'extension', 'fuente','created_at', 'updated_at') + fields = ('id', 'organizacion', 'pedimento', 'pedimento_numero', 'archivo', 'document_type', 'size', 'extension', 'fuente','fuente_nombre','created_at', 'updated_at') read_only_fields = ('id', 'size', 'extension', 'created_at', 'updated_at', 'pedimento_numero') def get_pedimento_numero(self, obj): @@ -26,6 +27,12 @@ class DocumentSerializer(serializers.ModelSerializer): raise serializers.ValidationError("Se requiere un archivo para subir") return value + def get_fuente_nombre(self, obj): + # Método 1: Si la fuente está precargada con select_related + if obj.fuente: + return obj.fuente.nombre + return "Desconocido" + class FuenteSerializer(serializers.ModelSerializer): class Meta: model = Fuente