diff --git a/.gitignore b/.gitignore index a386bea..e9e644d 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,4 @@ cython_debug/ #.idea/ # End of https://www.toptal.com/developers/gitignore/api/django - +*.bak diff --git a/Dockerfile b/Dockerfile index 22017e5..032e250 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,17 @@ FROM python:3.11-slim WORKDIR /app +# Instalar dependencias del sistema necesarias +RUN apt-get update && apt-get install -y --no-install-recommends wget && \ + wget https://www.rarlab.com/rar/rarlinux-x64-621.tar.gz && \ + tar -xzvf rarlinux*.tar.gz && \ + cp rar/unrar /usr/bin/unrar && \ + rm -rf rarlinux*.tar.gz rar + COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt - RUN pip install flower - COPY . . diff --git a/api/customs/views.py b/api/customs/views.py index 168d1e3..4bddd38 100644 --- a/api/customs/views.py +++ b/api/customs/views.py @@ -57,6 +57,22 @@ try: except ImportError: RAR_SUPPORT = False +def get_available_extractors(): + """ + Devuelve lista de extractores disponibles en orden de preferencia + """ + extractors = [] + if RAR_SUPPORT: + extractors.append('rarfile') + # Verificar si 'unrar' está disponible + if shutil.which('unrar'): + extractors.append('unrar') + # Verificar si '7z' o '7za' están disponibles + if shutil.which('7z'): + extractors.append('7z') + elif shutil.which('7za'): + extractors.append('7za') + return extractors def extract_rar_to_dir(rar_path, dest_dir): """ @@ -67,12 +83,29 @@ def extract_rar_to_dir(rar_path, dest_dir): Lanza Exception con mensaje explicativo si falla. """ - # Intento con rarfile (Python) - if RAR_SUPPORT: + + # Versión que primero verifica herramientas disponibles + available = get_available_extractors() + if not available: + raise Exception("No hay herramientas de extracción disponibles.") + + print(f"Extractores disponibles (en orden de preferencia): {available}") + + # Intento con rarfile primero si está disponible + # if RAR_SUPPORT: + if 'rarfile' in available and RAR_SUPPORT: try: # rarfile puede trabajar con rutas en disco mejor que con file-like with rarfile.RarFile(rar_path) as rf: rf.extractall(dest_dir) + + try: + if os.path.exists(rar_path): + os.remove(rar_path) + print(f"Archivo original eliminado: {rar_path}") + except OSError as remove_error: + print(f"Advertencia: No se pudo eliminar '{rar_path}': {remove_error}") + return except Exception as e: # Si rarfile falla (por ejemplo RarCannotExec), seguimos con herramientas externas @@ -87,18 +120,49 @@ def extract_rar_to_dir(rar_path, dest_dir): ['7za', 'x', rar_path, f'-o{dest_dir}', '-y'] ] - for cmd in external_cmds: - try: - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - return - except FileNotFoundError: - # El ejecutable no existe en PATH, intentar siguiente - continue - except subprocess.CalledProcessError as e: - # El comando falló en la extracción; intentar siguiente - print(f"External extractor failed ({cmd[0]}): {e}") + # for cmd in external_cmds: + # try: + # subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # try: + # if os.path.exists(rar_path): + # os.remove(rar_path) + # print(f"Archivo original eliminado: {rar_path}") + # except OSError as remove_error: + # print(f"Advertencia: No se pudo eliminar '{rar_path}': {remove_error}") + + # return + # except FileNotFoundError: + # # El ejecutable no existe en PATH, intentar siguiente + # continue + # except subprocess.CalledProcessError as e: + # # El comando falló en la extracción; intentar siguiente + # print(f"External extractor failed ({cmd[0]}): {e}") + # continue + + for extractor_name in available: + if extractor_name in external_cmds: + cmd = external_cmds[extractor_name] + try: + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + try: + if os.path.exists(rar_path): + os.remove(rar_path) + print(f"Archivo original eliminado: {rar_path}") + except OSError as remove_error: + print(f"Advertencia: No se pudo eliminar '{rar_path}': {remove_error}") + + return + except FileNotFoundError: + # El ejecutable no existe en PATH, intentar siguiente + continue + except subprocess.CalledProcessError as e: + # El comando falló en la extracción; intentar siguiente + print(f"External extractor {extractor_name} failed ({cmd[0]}): {e}") continue + # Si llegamos aquí, ningún método funcionó raise Exception("No se encontró una herramienta válida para extraer RAR (rarfile sin backend, 'unrar' o '7z' no disponibles o extracción fallida). Instale 'unrar' o 'p7zip' y asegúrese de que estén en PATH, o configure rarfile con un backend.") from .tasks.microservice_v2 import * @@ -212,7 +276,10 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app'] search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento'] - + # AGREGAR ESTOS CAMPOS PARA ORDENACIÓN + ordering_fields = ['created_at', 'pedimento', 'fecha_pago', 'aduana', 'patente'] + 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 @@ -444,6 +511,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada # 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 = [] @@ -591,7 +659,9 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada # Validar nomenclatura match = nomenclatura_pattern.match(folder_name) - if not match: + 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') @@ -602,25 +672,60 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada }) continue - 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}") - - # 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 + 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}") @@ -628,7 +733,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada # Verificar si el pedimento ya existe existing_pedimento = Pedimento.objects.filter( pedimento_app=pedimento_app, - organizacion=organizacion + # organizacion=organizacion ).first() print(f"Pedimento existente: {existing_pedimento is not None}") @@ -700,12 +805,32 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada # 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 + 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=4, # Fuente: Carga Plataforma archivo=django_file, size=len(file_content), extension=os.path.splitext(file_name)[1].lower().lstrip('.') diff --git a/api/record/views.py b/api/record/views.py index f2688f9..888aee2 100644 --- a/api/record/views.py +++ b/api/record/views.py @@ -76,7 +76,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin): modulo_efc = self.request.query_params.get('modulo') if modulo_efc: if modulo_efc == 'expedientes-detalle-pedimentos': - queryset = queryset.filter(document_type_id='11') + queryset = queryset.exclude(document_type_id__in=['1','2','3','4','5','6','7','8','9','10']) # Filtro personalizado por document_type # document_type = self.request.query_params.get('document_type') # if document_type: