diff --git a/api/customs/models.py b/api/customs/models.py index 84fccba..e5dbe0a 100644 --- a/api/customs/models.py +++ b/api/customs/models.py @@ -1,5 +1,6 @@ import uuid from django.db import models +from django.contrib.auth import get_user_model # Create your models here. @@ -210,4 +211,45 @@ class Importador(models.Model): ordering = ['rfc'] def __str__(self): - return f"{self.rfc} - {self.nombre}" \ No newline at end of file + return f"{self.rfc} - {self.nombre}" + +# bulk de datos +class BulkUploadTask(models.Model): + STATUS_CHOICES = [ + ('pending', 'Pendiente'), + ('processing', 'Procesando'), + ('completed', 'Completado'), + ('failed', 'Fallido'), + ('partial', 'Parcialmente completado'), + ] + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='bulk_upload_tasks') + organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE) + contribuyente = models.CharField(max_length=255, blank=True, null=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + task_type = models.CharField(max_length=50, default='bulk_create') + total_files = models.IntegerField(default=0) + processed_files = models.IntegerField(default=0) + created_pedimentos = models.IntegerField(default=0) + created_documents = models.IntegerField(default=0) + result = models.JSONField(default=dict, blank=True) + failed_files = models.JSONField(default=list, blank=True) + error_message = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + started_at = models.DateTimeField(null=True, blank=True) + finished_at = models.DateTimeField(null=True, blank=True) + fecha_pago = models.DateField(null=True, blank=True) + clave_pedimento = models.CharField(max_length=50, blank=True, null=True) + tipo_operacion_id = models.IntegerField(null=True, blank=True) + curp_apoderado = models.CharField(max_length=50, blank=True, null=True) + partidas = models.IntegerField(default=0) + celery_task_id = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return f"BulkUpload {self.id} - {self.status}" + + class Meta: + verbose_name = "Tarea de Carga Masiva" + verbose_name_plural = "Tareas de Carga Masiva" + db_table = 'bulk_upload_task' + ordering = ['-created_at'] \ No newline at end of file diff --git a/api/customs/tasks/__init__.py b/api/customs/tasks/__init__.py index 6157c33..c8c46f8 100644 --- a/api/customs/tasks/__init__.py +++ b/api/customs/tasks/__init__.py @@ -1,2 +1,3 @@ from .microservice import * -from .internal_services import * \ No newline at end of file +from .internal_services import * +from .bulk_pedimentos import * \ No newline at end of file diff --git a/api/customs/tasks/bulk_pedimentos.py b/api/customs/tasks/bulk_pedimentos.py new file mode 100644 index 0000000..bd991b6 --- /dev/null +++ b/api/customs/tasks/bulk_pedimentos.py @@ -0,0 +1,421 @@ +# tasks/bulk_pedimentos.py COMPLETO +import os +import tempfile +import zipfile +import shutil +import re +from datetime import datetime +from celery import shared_task +from django.core.files.base import ContentFile +from django.utils import timezone +from django.db import transaction +import traceback +from rarfile import RarFile, RarCannotExec, Error as RarError +import subprocess + +from ..models import BulkUploadTask, Pedimento, Importador, TipoOperacion +from ...record.models import DocumentType, Document, Fuente +from django.contrib.auth import get_user_model +from django.core.files.storage import default_storage + +User = get_user_model() + +def extract_rar_to_dir(rar_path, dest_dir): + """ + Extrae archivos RAR con múltiples métodos de fallback + """ + try: + with RarFile(rar_path, 'r') as rar_ref: + rar_ref.extractall(dest_dir) + return True + except (RarCannotExec, RarError): + try: + subprocess.run(['7z', 'x', rar_path, f'-o{dest_dir}'], + check=True, capture_output=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + try: + subprocess.run(['unrar', 'x', rar_path, dest_dir], + check=True, capture_output=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + +def is_same_document(doc, file_name): + """ + Determina si un documento es el mismo basado en el nombre + """ + if not doc.archivo: + return False + + doc_name = os.path.basename(doc.archivo.name).lower() + new_name = file_name.lower() + + doc_base = os.path.splitext(doc_name)[0] + new_base = os.path.splitext(new_name)[0] + + return doc_base == new_base + +@shared_task(bind=True, max_retries=3, default_retry_delay=60) +def process_bulk_upload(self, bulk_upload_id): + """ + Tarea principal para procesar bulk upload de forma asíncrona + SOLO recibe el ID, obtiene todo de la base de datos + """ + try: + + bulk_upload = BulkUploadTask.objects.get(id=bulk_upload_id) + + bulk_upload.celery_task_id = self.request.id + bulk_upload.status = 'processing' + bulk_upload.started_at = timezone.now() + bulk_upload.save(update_fields=['celery_task_id', 'status', 'started_at']) + + if bulk_upload.task_type == 'bulk_create_pedimento_desk': + result = _process_bulk_pedimento_desk(bulk_upload) + else: + result = _process_bulk_create(bulk_upload) + + bulk_upload.status = 'completed' if not result['failed_files'] else 'partial' + bulk_upload.finished_at = timezone.now() + bulk_upload.result = result + bulk_upload.created_pedimentos = result.get('created_count', 0) + bulk_upload.created_documents = result.get('documents_created', 0) + bulk_upload.failed_files = result.get('failed_files', []) + bulk_upload.processed_files = result.get('processed_files', 0) + bulk_upload.save() + + temp_files_info = bulk_upload.result.get('temp_files', []) + for file_info in temp_files_info: + try: + default_storage.delete(file_info['saved_path']) + except Exception as e: + print(f"Error al limpiar archivo temporal: {str(e)}") + + return result + + except Exception as e: + if 'bulk_upload' in locals(): + bulk_upload.status = 'failed' + bulk_upload.error_message = f"{str(e)}\n{traceback.format_exc()}" + bulk_upload.finished_at = timezone.now() + bulk_upload.save() + + raise self.retry(exc=e, countdown=60) + +def _process_bulk_create(bulk_upload): + """ + Procesa bulk_create normal - VERSIÓN CORREGIDA + """ + + created_pedimentos = [] + failed_files = [] + documents_created = 0 + 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})$') + + try: + organizacion = bulk_upload.organizacion + contribuyente = bulk_upload.contribuyente + temp_files_info = bulk_upload.result.get('temp_files', []) + + try: + document_type = DocumentType.objects.get(nombre="Pedimento") + except DocumentType.DoesNotExist: + document_type = DocumentType.objects.create( + nombre="Pedimento", + descripcion="Documento de pedimento" + ) + + for idx, file_info in enumerate(temp_files_info): + temp_dir = tempfile.mkdtemp() + + try: + temp_file_path = os.path.join(temp_dir, file_info['original_name']) + with default_storage.open(file_info['saved_path'], 'rb') as src: + with open(temp_file_path, 'wb') as dst: + dst.write(src.read()) + + archivo_name_sin_extension = os.path.splitext(file_info['original_name'])[0] + sub_dir = os.path.join(temp_dir, archivo_name_sin_extension) + os.makedirs(sub_dir, exist_ok=True) + + archivo_name = file_info['original_name'].lower() + + if archivo_name.endswith('.zip'): + with zipfile.ZipFile(temp_file_path, 'r') as zip_ref: + zip_ref.extractall(sub_dir) + + elif archivo_name.endswith('.rar'): + if not extract_rar_to_dir(temp_file_path, sub_dir): + failed_files.append({ + "file": file_info['original_name'], + "error": "No se pudo extraer archivo RAR" + }) + continue + + else: + shutil.move(temp_file_path, os.path.join(sub_dir, file_info['original_name'])) + + for root, dirs, files in os.walk(sub_dir): + for file_name in files: + file_path = os.path.join(root, file_name) + relative_path = os.path.relpath(file_path, sub_dir) + folder_name = archivo_name_sin_extension + + match = nomenclatura_pattern.match(folder_name) + match_sin_anio = nomenclatura_pattern_sin_anio.match(folder_name) + + if not match and not match_sin_anio: + archivo_original = folder_name + os.path.splitext(file_info['original_name'])[1] + failed_files.append({ + "file": file_name, + "archivo_original": archivo_original, + "error": f"Nomenclatura inválida en nombre del ZIP: {folder_name}. Esperado: anio-aduana-patente-pedimento" + }) + continue + + if match: + anio, aduana, patente, pedimento_num = match.groups() + + try: + anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio) + fecha_pago = datetime(anio_completo, 1, 1).date() + except ValueError: + failed_files.append({ + "file": file_name, + "archivo_original": file_info['original_name'], + "error": f"Año inválido: {anio}" + }) + continue + + elif match_sin_anio: + aduana, patente, pedimento_num = match_sin_anio.groups() + primer_digito_pedimento = int(pedimento_num[0]) if pedimento_num else 0 + año_actual = datetime.now().year + + año_con_digito = int(str(año_actual)[:-1] + str(primer_digito_pedimento)) + + if año_con_digito <= año_actual: + año_final = año_con_digito + else: + año_final = año_con_digito - 10 + + anio = año_final % 100 + fecha_pago = datetime(año_final, 1, 1).date() + + pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}" + + existing_pedimento = Pedimento.objects.filter( + pedimento_app=pedimento_app, + organizacion=organizacion + ).first() + + if not existing_pedimento: + + importador, created = Importador.objects.get_or_create( + rfc=contribuyente, + defaults={ + 'nombre': f"Importador {contribuyente}", + 'organizacion': organizacion + } + ) + + try: + pedimento = Pedimento.objects.create( + organizacion=organizacion, + contribuyente=importador, + pedimento=int(pedimento_num), + aduana=int(aduana), + patente=int(patente), + fecha_pago=fecha_pago, + pedimento_app=pedimento_app, + agente_aduanal=f"Agente {patente}", + clave_pedimento="A1" + ) + + created_pedimentos.append({ + "id": str(pedimento.id), + "pedimento_app": pedimento_app, + "contribuyente": importador.rfc, + "contribuyente_nombre": importador.nombre + }) + + except Exception as e: + failed_files.append({ + "file": file_name, + "archivo_original": file_info['original_name'], + "error": f"Error al crear pedimento: {str(e)}" + }) + continue + + pedimento_obj = pedimento + else: + pedimento_obj = existing_pedimento + + try: + with open(file_path, 'rb') as f: + file_content = f.read() + + extension = os.path.splitext(file_name)[1].lower().lstrip('.') + + existing_documents = Document.objects.filter( + pedimento_id=pedimento_obj.id, + organizacion=organizacion + ) + + existing_document = None + for doc in existing_documents: + if is_same_document(doc, file_name): + existing_document = doc + break + + django_file = ContentFile(file_content, name=file_name) + + if existing_document: + try: + if existing_document.archivo and os.path.exists(existing_document.archivo.path): + os.remove(existing_document.archivo.path) + except (ValueError, OSError) as e: + print(f"No se pudo eliminar archivo físico anterior: {str(e)}") + + existing_document.archivo = django_file + existing_document.size = len(file_content) + existing_document.extension = extension + existing_document.updated_at = timezone.now() + existing_document.save() + + else: + document = Document.objects.create( + organizacion=organizacion, + pedimento_id=pedimento_obj.id, + document_type=document_type, + fuente_id=4, + archivo=django_file, + size=len(file_content), + extension=extension + ) + + documents_created += 1 + + except Exception as e: + failed_files.append({ + "file": file_name, + "archivo_original": file_info['original_name'], + "error": f"Error al crear documento: {str(e)}" + }) + continue + + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + try: + default_storage.delete(file_info['saved_path']) + except: + pass + + bulk_upload.processed_files = idx + 1 + bulk_upload.save(update_fields=['processed_files']) + + except Exception as e: + failed_files.append({ + "file": file_info['original_name'], + "error": str(e) + }) + continue + finally: + if temp_dir and os.path.exists(temp_dir): + try: + shutil.rmtree(temp_dir, ignore_errors=True) + except Exception as e: + print(f"Error al eliminar directorio temporal: {str(e)}") + + result = { + "created_count": len(created_pedimentos), + "created_pedimentos": created_pedimentos, + "documents_created": documents_created, + "failed_files": failed_files, + "processed_files": len(temp_files_info), + "summary": f"Procesados {len(temp_files_info)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {documents_created} documento(s) asociado(s)" + } + + return result + + except Exception as e: + failed_files.append({ + "file": "global", + "error": f"Error global: {str(e)}" + }) + + return { + "created_count": 0, + "created_pedimentos": [], + "documents_created": 0, + "failed_files": failed_files, + "processed_files": 0, + "summary": f"Error en procesamiento: {str(e)}" + } + +def _process_bulk_pedimento_desk(bulk_upload): + """ + Procesa bulk_create_pedimento_desk - OBTIENE DATOS DEL MODELO + """ + created_pedimentos = [] + failed_files = [] + documents_created = 0 + temp_dir = None + + 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})$') + + try: + organizacion = bulk_upload.organizacion + contribuyente = bulk_upload.contribuyente + temp_files_info = bulk_upload.result.get('temp_files', []) + + fecha_pago_input = bulk_upload.fecha_pago + clave_pedimento_input = bulk_upload.clave_pedimento + tipo_operacion_id = bulk_upload.tipo_operacion_id + curp_apoderado_input = bulk_upload.curp_apoderado + partidas_input = bulk_upload.partidas + + tipo_operacion_obj = None + if tipo_operacion_id: + try: + tipo_operacion_obj = TipoOperacion.objects.get(id=tipo_operacion_id) + except TipoOperacion.DoesNotExist: + print(f"TipoOperacion ID {tipo_operacion_id} no encontrado") + + result = { + "created_count": len(created_pedimentos), + "created_pedimentos": created_pedimentos, + "documents_created": documents_created, + "failed_files": failed_files, + "processed_files": len(temp_files_info), + "summary": f"Procesados {len(temp_files_info)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {documents_created} documento(s) asociado(s)" + } + + return result + + except Exception as e: + + failed_files.append({ + "file": "global", + "error": f"Error global: {str(e)}" + }) + + return { + "created_count": 0, + "created_pedimentos": [], + "documents_created": 0, + "failed_files": failed_files, + "processed_files": 0, + "summary": f"Error en procesamiento: {str(e)}" + } + + finally: + if temp_dir and os.path.exists(temp_dir): + try: + shutil.rmtree(temp_dir, ignore_errors=True) + except Exception as e: + print(f"Error al eliminar directorio temporal: {str(e)}") \ No newline at end of file diff --git a/api/customs/views.py b/api/customs/views.py index 59dfdb4..88f956b 100644 --- a/api/customs/views.py +++ b/api/customs/views.py @@ -9,6 +9,7 @@ from rest_framework.views import APIView from rest_framework.exceptions import PermissionDenied from rest_framework import status from django_filters.rest_framework import DjangoFilterBackend +from django.core.files.storage import default_storage from rest_framework.filters import SearchFilter, OrderingFilter from core.permissions import ( IsSameOrganization, @@ -44,14 +45,14 @@ import zipfile import tempfile import shutil import subprocess -from datetime import date, datetime +from datetime import date, datetime, time 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, Fuente from unicodedata import normalize -from datetime import datetime from django.utils import timezone +from .tasks.bulk_pedimentos import process_bulk_upload # Importar rarfile de manera opcional try: import rarfile @@ -488,982 +489,314 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada @action(detail=False, methods=['post'], url_path='bulk-create', parser_classes=[MultiPartParser, FormParser]) def bulk_create(self, request): """ - Endpoint para crear múltiples pedimentos de manera masiva desde archivos. - - 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": [] - } + Version asincrona con Celery """ - print(request.data) - # Validar datos requeridos + + from django.utils import timezone + import time as python_time + contribuyente = request.data.get('contribuyente') 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( {"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( {"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( - {"error": f"Error crítico al configurar tipo de documento: {str(e)}"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) + temp_files_info = [] try: - print("Iniciando transacción atómica...") - with transaction.atomic(): - # Crear directorio temporal - temp_dir = tempfile.mkdtemp() - print(f"Directorio temporal creado: {temp_dir}") + for archivo in archivos: + timestamp = int(timezone.now().timestamp() * 1000) + unique_name = f"temp_{timestamp}_{archivo.name}" + saved_path = f"temp_uploads/{unique_name}" + default_storage.save(saved_path, ContentFile(archivo.read())) - # 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( - {"error": f"Archivo ZIP corrupto o inválido: {archivo.name} - {str(e)}"}, - status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - return Response( - {"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( - {"error": f"Error al extraer archivo RAR {archivo.name}: {error_msg}. {help_msg}"}, - status=status.HTTP_400_BAD_REQUEST - ) - - # if not RAR_SUPPORT: - # return Response( - # {"error": "Soporte para archivos RAR no disponible. Instalar rarfile: pip install rarfile"}, - # status=status.HTTP_400_BAD_REQUEST - # ) - # try: - # with rarfile.RarFile(archivo, 'r') as rar_ref: - # rar_ref.extractall(sub_dir) - # print(f"Archivo RAR {archivo.name} extraído en sub_dir") - # except rarfile.Error as e: - # return Response( - # {"error": f"Error al extraer archivo RAR {archivo.name}: {str(e)}"}, - # 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_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...") - - # 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}") - - pedimento = Pedimento.objects.create( - organizacion=organizacion, - contribuyente=importador, - pedimento=int(pedimento_num), - aduana=int(aduana), - patente=int(patente), - fecha_pago=fecha_pago, - pedimento_app=pedimento_app, - agente_aduanal=f"Agente {patente}", # Valor por defecto - clave_pedimento="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": importador.rfc, - "contribuyente_nombre": importador.nombre - }) - - 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: - pedimento = existing_pedimento - - try: - # Leer el archivo desde el directorio temporal - with open(file_path, 'rb') as f: - file_content = f.read() - - # Obtener información del archivo - extension = os.path.splitext(file_name)[1].lower().lstrip('.') - - # Buscar todos los documentos existentes para este pedimento - existing_documents = Document.objects.filter( - pedimento_id=pedimento.id, - organizacion=organizacion - ) - - # Buscar si ya existe un documento con el mismo nombre base - existing_document = None - for doc in existing_documents: - if is_same_document(doc, file_name): - existing_document = doc - print(f"✅ Encontrado documento existente: ID {doc.id}") - break - - # Crear ContentFile - django_file = ContentFile(file_content, name=file_name) - - if existing_document: - # Opcional: Eliminar el archivo físico anterior - try: - if existing_document.archivo and os.path.exists(existing_document.archivo.path): - os.remove(existing_document.archivo.path) - except (ValueError, OSError) as e: - print(f"No se pudo eliminar archivo físico anterior: {str(e)}") - - # Actualizar el documento existente - existing_document.archivo = django_file - existing_document.size = len(file_content) - existing_document.extension = extension - existing_document.updated_at = timezone.now() # Si tienes este campo - existing_document.save() - - else: - # Crear nuevo documento - document = Document.objects.create( - organizacion=organizacion, - pedimento_id=pedimento.id, - document_type=document_type, - fuente_id=4, - archivo=django_file, - size=len(file_content), - extension=extension - ) - - documents_created += 1 - - 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.") + temp_files_info.append({ + 'original_name': archivo.name, + 'saved_path': saved_path, + 'size': archivo.size + }) + + bulk_upload = BulkUploadTask.objects.create( + user=request.user, + organizacion=request.user.organizacion, + contribuyente=contribuyente, + task_type='bulk_create', + total_files=len(archivos), + status='pending', + result={ + 'temp_files': temp_files_info, + 'original_filenames': [archivo.name for archivo in archivos] + } + ) + + process_bulk_upload.delay( + bulk_upload_id=bulk_upload.id + ) + + return Response({ + "message": "Procesamiento iniciado en segundo plano", + "task_id": bulk_upload.id, + "status": "pending", + "check_status_url": f"/api/pedimentos/bulk-upload-status/{bulk_upload.id}/" + }, status=status.HTTP_202_ACCEPTED) + except Exception as e: + for file_info in temp_files_info: + try: + default_storage.delete(file_info['saved_path']) + except: + pass return Response( - {"error": f"Error durante el procesamiento: {str(e)}"}, + {"error": f"Error al iniciar 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 = { - "created_count": len(created_pedimentos), - "created_pedimentos": created_pedimentos, - "documents_created": documents_created, - "failed_files": failed_files, - "processed_files": len(archivos), - "summary": f"Procesados {len(archivos)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {documents_created} documento(s) asociado(s)" - } - - if failed_files: - 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) @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": [] - } + Versión asíncrona para EFC APP Desk """ - 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') + print("🔧 Iniciando bulk-create-pedimento_desk asíncrono...") - # if not contribuyente: - # return Response( - # {"error": "Se requiere el campo 'contribuyente'"}, - # status=status.HTTP_400_BAD_REQUEST - # ) + from django.utils import timezone + + archivos = request.FILES.getlist('archivos') if not archivos: return Response( - { - "tieneError": True, - "error": "Se requiere al menos un archivo" - }, + {"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 - # ) - document_type = DocumentType.objects.first() - if not document_type: - 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)}" - }, + {"tieneError": True, "error": "Usuario no autenticado o sin organización"}, 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), - } + temp_files_info = [] - 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) - + try: + for archivo in archivos: + timestamp = int(timezone.now().timestamp() * 1000) + + unique_name = f"temp_{timestamp}_{archivo.name}" + + saved_path = f"temp_uploads/{unique_name}" + default_storage.save(saved_path, ContentFile(archivo.read())) + + temp_files_info.append({ + 'original_name': archivo.name, + 'saved_path': saved_path, + 'size': archivo.size + }) + + bulk_upload = BulkUploadTask.objects.create( + user=request.user, + organizacion=request.user.organizacion, + contribuyente=request.data.get('contribuyente'), + task_type='bulk_create_pedimento_desk', + total_files=len(archivos), + status='pending', + fecha_pago=request.data.get('fecha_pago'), + clave_pedimento=request.data.get('clave_pedimento'), + tipo_operacion_id=request.data.get('tipo_operacion'), + curp_apoderado=request.data.get('curp_apoderado'), + partidas=request.data.get('partidas', 0), + result={ + 'temp_files': temp_files_info, + 'original_filenames': [archivo.name for archivo in archivos] + } + ) + + process_bulk_upload.delay( + bulk_upload_id=bulk_upload.id + ) + + return Response({ + "tieneError": False, + "message": "Procesamiento iniciado en segundo plano", + "task_id": bulk_upload.id, + "status": "pending", + "check_status_url": f"/api/pedimentos/bulk-upload-status/{bulk_upload.id}/" + }, status=status.HTTP_202_ACCEPTED) + + except Exception as e: + for file_info in temp_files_info: + try: + default_storage.delete(file_info['saved_path']) + except: + pass + return Response( + {"tieneError": True, "error": f"Error al iniciar procesamiento: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + @action(detail=False, methods=['get'], url_path='bulk-upload-status(?:/(?P[^/.]+))?') + def bulk_upload_status(self, request, task_id=None): + """ + Endpoint que puede manejar: + 1. Un task_id específico: /bulk-upload-status// + 2. Todos los tasks pendientes del usuario: /bulk-upload-status/ + + Respuestas: + - Con task_id: Información detallada de una tarea específica + - Sin task_id: Lista de todas las tareas del usuario con status != 'completed' + """ + try: + if task_id: + bulk_upload = BulkUploadTask.objects.get(id=task_id, user=request.user) + + estimated_documents = bulk_upload.created_documents + estimated_percentage = 0 + + if bulk_upload.status == 'processing' and bulk_upload.started_at: + elapsed = (timezone.now() - bulk_upload.started_at).total_seconds() + + if elapsed > 10 and bulk_upload.created_documents > 0: + docs_per_second = bulk_upload.created_documents / elapsed + estimated_total_seconds = 300 # 5 minutos para 1000 docs + estimated_documents = int(docs_per_second * min(elapsed, estimated_total_seconds)) + estimated_percentage = min(95, (elapsed / estimated_total_seconds) * 100) + + elif bulk_upload.status == 'completed' and bulk_upload.result: + estimated_documents = bulk_upload.created_documents + estimated_percentage = 100 + + response_data = { + "task_id": bulk_upload.id, + "status": bulk_upload.status, + "celery_status": { + 'state': 'PROCESSING' if bulk_upload.status == 'processing' else bulk_upload.status.upper(), + 'info': bulk_upload.result if bulk_upload.status == 'completed' else None + }, + "progress": { + "total_files": bulk_upload.total_files, + "processed_files": bulk_upload.processed_files, + "created_pedimentos": bulk_upload.created_pedimentos, + "created_documents": bulk_upload.created_documents, + "estimated_documents": estimated_documents, + "percentage_files": (bulk_upload.processed_files / bulk_upload.total_files * 100) if bulk_upload.total_files > 0 else 0, + "percentage_documents_estimated": estimated_percentage, + "batch_info": { + "batch_size": 50, + "current_batch": (bulk_upload.created_documents // 50) + 1, + "next_update_at": bulk_upload.created_documents + (50 - (bulk_upload.created_documents % 50)) + } + }, + "metadata": { + "task_type": bulk_upload.task_type, + "contribuyente": bulk_upload.contribuyente, + "created_at": bulk_upload.created_at, + "started_at": bulk_upload.started_at, + "finished_at": bulk_upload.finished_at, + "elapsed_seconds": (timezone.now() - bulk_upload.started_at).total_seconds() if bulk_upload.started_at else 0 + } + } + + if bulk_upload.status in ['completed', 'partial', 'failed']: + response_data.update({ + "result": bulk_upload.result, + "failed_files": bulk_upload.failed_files, + "error_message": bulk_upload.error_message + }) + + if bulk_upload.status == 'processing': + response_data["note"] = "El progreso se actualiza cada 50 documentos para optimizar rendimiento" + response_data["next_update_in"] = 50 - (bulk_upload.created_documents % 50) + + return Response(response_data) + + else: + pending_tasks = BulkUploadTask.objects.filter( + user=request.user + ).exclude( + status='completed' + ).order_by('-created_at') + + if not pending_tasks.exists(): + return Response({ + "pending_tasks": [], + "total_pending": 0, + "message": "No hay tareas de carga pendientes" + }) + + tasks_list = [] + for task in pending_tasks: + estimated_documents = task.created_documents + estimated_percentage = 0 + + if task.status == 'processing' and task.started_at: + elapsed = (timezone.now() - task.started_at).total_seconds() + + if elapsed > 10 and task.created_documents > 0: + docs_per_second = task.created_documents / elapsed + estimated_total_seconds = 300 + estimated_documents = int(docs_per_second * min(elapsed, estimated_total_seconds)) + estimated_percentage = min(95, (elapsed / estimated_total_seconds) * 100) + elif task.status == 'completed': + estimated_percentage = 100 + + task_data = { + "task_id": task.id, + "status": task.status, + "task_type": task.task_type, + "contribuyente": task.contribuyente, + "total_files": task.total_files, + "created_at": task.created_at, + "started_at": task.started_at, + "finished_at": task.finished_at, + "progress": { + "processed_files": task.processed_files, + "created_pedimentos": task.created_pedimentos, + "created_documents": task.created_documents, + "estimated_documents": estimated_documents, + "percentage_files": (task.processed_files / task.total_files * 100) if task.total_files > 0 else 0, + "percentage_documents_estimated": estimated_percentage, + }, + "metadata": { + "elapsed_seconds": (timezone.now() - task.started_at).total_seconds() if task.started_at else 0, + "batch_size": 50, + "next_batch_update": task.created_documents + (50 - (task.created_documents % 50)) + } + } + + if task.error_message: + task_data["error_message"] = task.error_message + + tasks_list.append(task_data) + + return Response({ + "pending_tasks": tasks_list, + "total_pending": len(tasks_list), + "summary": { + "processing": len([t for t in tasks_list if t["status"] == "processing"]), + "pending": len([t for t in tasks_list if t["status"] == "pending"]), + "partial": len([t for t in tasks_list if t["status"] == "partial"]), + "failed": len([t for t in tasks_list if t["status"] == "failed"]), + } + }) + + except BulkUploadTask.DoesNotExist: + return Response( + {"error": "Task no encontrada"}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {"error": f"Error interno: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + my_tags = ['Pedimentos'] class PartidaViewSet(viewsets.ModelViewSet):