fix: Se ajusta carga de expediente en RAR y se agega validacion para evitar duplicar el pedimento independientemente de la organizacion de carga.

This commit is contained in:
2025-12-10 11:30:58 -07:00
parent 202b053698
commit ed63a4854c
4 changed files with 167 additions and 37 deletions

2
.gitignore vendored
View File

@@ -178,4 +178,4 @@ cython_debug/
#.idea/ #.idea/
# End of https://www.toptal.com/developers/gitignore/api/django # End of https://www.toptal.com/developers/gitignore/api/django
*.bak

View File

@@ -3,12 +3,17 @@ FROM python:3.11-slim
WORKDIR /app 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 ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN pip install flower RUN pip install flower
COPY . . COPY . .

View File

@@ -57,6 +57,22 @@ try:
except ImportError: except ImportError:
RAR_SUPPORT = False 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): 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. 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: try:
# rarfile puede trabajar con rutas en disco mejor que con file-like # rarfile puede trabajar con rutas en disco mejor que con file-like
with rarfile.RarFile(rar_path) as rf: with rarfile.RarFile(rar_path) as rf:
rf.extractall(dest_dir) 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 return
except Exception as e: except Exception as e:
# Si rarfile falla (por ejemplo RarCannotExec), seguimos con herramientas externas # 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'] ['7za', 'x', rar_path, f'-o{dest_dir}', '-y']
] ]
for cmd in external_cmds: # for cmd in external_cmds:
try: # try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return
except FileNotFoundError: # try:
# El ejecutable no existe en PATH, intentar siguiente # if os.path.exists(rar_path):
continue # os.remove(rar_path)
except subprocess.CalledProcessError as e: # print(f"Archivo original eliminado: {rar_path}")
# El comando falló en la extracción; intentar siguiente # except OSError as remove_error:
print(f"External extractor failed ({cmd[0]}): {e}") # 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 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.") 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 * 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'] 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'] 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): def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador 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 # Regex para validar nomenclatura: anio-aduana-patente-pedimento
nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$') 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 = [] created_pedimentos = []
failed_files = [] failed_files = []
@@ -591,7 +659,9 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Validar nomenclatura # Validar nomenclatura
match = nomenclatura_pattern.match(folder_name) 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}") print(f"Nomenclatura inválida: {folder_name}")
# Determinar el archivo original basado en el subdirectorio # 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') 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 continue
print(f"Nomenclatura válida: {folder_name}") if match:
anio, aduana, patente, pedimento_num = match.groups()
print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") print(f"Nomenclatura válida: {folder_name}")
anio, aduana, patente, pedimento_num = match.groups()
# Crear fecha_pago basada en el año print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
try: # Formato original: anio-aduana-patente-pedimento
# Convertir año de 2 dígitos a 4 dígitos # Crear fecha_pago basada en el año
anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio) try:
fecha_pago = datetime(anio_completo, 1, 1).date() # Convertir año de 2 dígitos a 4 dígitos
print(f"Fecha de pago calculada: {fecha_pago}") anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio)
except ValueError: fecha_pago = datetime(anio_completo, 1, 1).date()
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') print(f"Fecha de pago calculada: {fecha_pago}")
failed_files.append({ except ValueError:
"file": relative_path, archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
"archivo_original": archivo_original, failed_files.append({
"error": f"Año inválido: {anio}" "file": relative_path,
}) "archivo_original": archivo_original,
continue "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 # Generar pedimento_app
pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}" pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}"
print(f"Pedimento_app generado: {pedimento_app}") print(f"Pedimento_app generado: {pedimento_app}")
@@ -628,7 +733,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Verificar si el pedimento ya existe # Verificar si el pedimento ya existe
existing_pedimento = Pedimento.objects.filter( existing_pedimento = Pedimento.objects.filter(
pedimento_app=pedimento_app, pedimento_app=pedimento_app,
organizacion=organizacion # organizacion=organizacion
).first() ).first()
print(f"Pedimento existente: {existing_pedimento is not None}") 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 # Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name) 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}") print(f"Creando documento para archivo: {file_name}")
# Crear documento - Django automáticamente guardará el archivo en media/documents/ # Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create( document = Document.objects.create(
organizacion=organizacion, organizacion=organizacion,
pedimento_id=pedimento.id, pedimento_id=pedimento.id,
document_type=document_type, document_type=document_type,
fuente_id=4, # Fuente: Carga Plataforma
archivo=django_file, archivo=django_file,
size=len(file_content), size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.') extension=os.path.splitext(file_name)[1].lower().lstrip('.')

View File

@@ -76,7 +76,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
modulo_efc = self.request.query_params.get('modulo') modulo_efc = self.request.query_params.get('modulo')
if modulo_efc: if modulo_efc:
if modulo_efc == 'expedientes-detalle-pedimentos': 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 # Filtro personalizado por document_type
# document_type = self.request.query_params.get('document_type') # document_type = self.request.query_params.get('document_type')
# if document_type: # if document_type: