4 Commits

8 changed files with 950 additions and 46 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

@@ -3,8 +3,16 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# Instalar dependencias del sistema # Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \ # RUN apt-get update && apt-get install -y \
# supervisor \
# && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends \
supervisor \ supervisor \
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 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copiar e instalar dependencias de Python # Copiar e instalar dependencias de Python

View File

@@ -9,6 +9,7 @@ from api.customs.models import (
Partida Partida
) )
from django.db import models from django.db import models
from django.db.models import Q
from api.record.models import Document # Asegúrate de importar el modelo Documento from api.record.models import Document # Asegúrate de importar el modelo Documento
from api.record.serializers import DocumentSerializer from api.record.serializers import DocumentSerializer
from api.vucem.serializers import VucemSerializer from api.vucem.serializers import VucemSerializer
@@ -43,6 +44,59 @@ class PedimentoSerializer(serializers.ModelSerializer):
return rep return rep
class PartidaSerializer(serializers.ModelSerializer): class PartidaSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
def get_documentos(self, obj):
"""
Busca documentos en la tabla `document` que coincidan EXACTAMENTE con:
'documents/vu_PT_{pedimentoApp}_{numero}' al inicio del nombre del archivo.
"""
if not obj or not getattr(obj, 'pedimento', None):
return []
if not obj or not getattr(obj, 'numero_partida', None):
return []
try:
pedimentoApp = str(obj.pedimento.pedimento_app).strip()
numero = str(obj.numero_partida).strip()
# Construir el patrón exacto de búsqueda
patron_exacto = f'documents/vu_PT_{pedimentoApp}_{numero}.xml'
# Buscar documentos que empiecen EXACTAMENTE con ese patrón
qs = Document.objects.filter(
archivo=patron_exacto
)
# Opción 2: Si puede tener diferentes extensiones
# patron_base = f'documents/vu_PT_{pedimentoApp}_{numero}'
# qs = Document.objects.filter(
# archivo__startswith=patron_base
# ).filter(
# archivo__in=[
# f'{patron_base}.xml',
# f'{patron_base}.pdf',
# f'{patron_base}.zip'
# ]
# )
# Filtro adicional por pedimento si el modelo Document tiene este campo
if hasattr(Document, 'pedimento'):
qs = qs.filter(pedimento=obj.pedimento)
# Filtro por organización
if hasattr(obj, 'organizacion') and obj.organizacion:
qs = qs.filter(organizacion=obj.organizacion)
serializer = DocumentSerializer(qs, many=True, context=self.context)
return serializer.data
#return []
except Exception:
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
return []
class Meta: class Meta:
model = Partida model = Partida
fields = '__all__' fields = '__all__'
@@ -129,6 +183,47 @@ class ProcesamientoPedimentoSerializer(serializers.ModelSerializer):
return representation return representation
class EDocumentSerializer(serializers.ModelSerializer): class EDocumentSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
def get_documentos(self, obj):
"""
Busca documentos en la tabla `document` que coincidan con el
`numero_edocument` dentro del nombre del archivo (`archivo`). Se
filtra por organización para evitar devolver documentos de otras orgs.
Devuelve la serialización completa de los documentos encontrados:
1. Empiecen con 'vu_EDOCUMENT' en el nombre del archivo
2. Terminen con el numero_edocument + .xml
3. Pertenezcan a la misma organización
"""
if not obj or not getattr(obj, 'numero_edocument', None):
return []
if not obj or not getattr(obj, 'pedimento', None):
return []
# if not obj or not getattr(obj, 'pedimento_id', None):
# return []
try:
numero = str(obj.numero_edocument).strip()
# id_pedimento = str(obj.pedimento_id).strip()
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
)
# Filtro por organización si aplica
if hasattr(obj, 'organizacion') and obj.organizacion:
qs = qs.filter(organizacion=obj.organizacion)
serializer = DocumentSerializer(qs, many=True, context=self.context)
return serializer.data
except Exception:
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
return []
class Meta: class Meta:
model = EDocument model = EDocument
fields = '__all__' fields = '__all__'
@@ -142,11 +237,48 @@ class EDocumentSerializer(serializers.ModelSerializer):
self.fields['organizacion'].read_only = True self.fields['organizacion'].read_only = True
class CoveSerializer(serializers.ModelSerializer): class CoveSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
class Meta: class Meta:
model = Cove model = Cove
fields = '__all__' fields = '__all__'
read_only_fields = ('created_at', 'updated_at') read_only_fields = ('created_at', 'updated_at')
def get_documentos(self, obj):
"""
Busca documentos en la tabla `document` que coincidan con el
`numero_cove` dentro del nombre del archivo (`archivo`). Se
filtra por organización para evitar devolver documentos de otras orgs.
Devuelve la serialización completa de los documentos encontrados:
1. Empiecen con 'vu_COVE' en el nombre del archivo
2. Terminen con el numero_cove + .xml
3. Pertenezcan a la misma organización
"""
if not obj or not getattr(obj, 'numero_cove', None):
return []
if not obj or not getattr(obj, 'pedimento', None):
return []
try:
numero = str(obj.numero_cove).strip()
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
)
# Filtro por organización si aplica
if hasattr(obj, 'organizacion') and obj.organizacion:
qs = qs.filter(organizacion=obj.organizacion)
serializer = DocumentSerializer(qs, many=True, context=self.context)
return serializer.data
except Exception:
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
return []
class ImportadorSerializer(serializers.ModelSerializer): class ImportadorSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Importador model = Importador

View File

@@ -23,7 +23,7 @@ from api.customs.models import (
EDocument, EDocument,
Cove, Cove,
Importador, Importador,
Partida Partida,
) )
from api.customs.serializers import ( from api.customs.serializers import (
PedimentoSerializer, PedimentoSerializer,
@@ -44,11 +44,11 @@ import zipfile
import tempfile import tempfile
import shutil import shutil
import subprocess import subprocess
from datetime import datetime from datetime import date, datetime
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
from rest_framework.parsers import MultiPartParser, FormParser 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 # Importar rarfile de manera opcional
try: try:
@@ -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:
# 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: try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 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 return
except FileNotFoundError: except FileNotFoundError:
# El ejecutable no existe en PATH, intentar siguiente # El ejecutable no existe en PATH, intentar siguiente
continue continue
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# El comando falló en la extracción; intentar siguiente # El comando falló en la extracción; intentar siguiente
print(f"External extractor failed ({cmd[0]}): {e}") 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,9 +276,21 @@ 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
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): def perform_create(self, serializer):
""" """
@@ -444,6 +520,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 +668,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,10 +681,12 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
}) })
continue continue
if match:
print(f"Nomenclatura válida: {folder_name}") print(f"Nomenclatura válida: {folder_name}")
anio, aduana, patente, pedimento_num = match.groups() anio, aduana, patente, pedimento_num = match.groups()
print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") 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 # Crear fecha_pago basada en el año
try: try:
# Convertir año de 2 dígitos a 4 dígitos # Convertir año de 2 dígitos a 4 dígitos
@@ -621,6 +702,39 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
}) })
continue 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 +742,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 +814,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('.')
@@ -758,6 +892,542 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
return Response(response_data, status=response_status) 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'] my_tags = ['Pedimentos']

View File

@@ -9,10 +9,11 @@ from api.customs.models import Pedimento
class DocumentSerializer(serializers.ModelSerializer): class DocumentSerializer(serializers.ModelSerializer):
pedimento_numero = serializers.SerializerMethodField(read_only=True) pedimento_numero = serializers.SerializerMethodField(read_only=True)
pedimento = serializers.PrimaryKeyRelatedField(queryset=Pedimento.objects.all()) pedimento = serializers.PrimaryKeyRelatedField(queryset=Pedimento.objects.all())
fuente_nombre = serializers.SerializerMethodField()
fuente = serializers.PrimaryKeyRelatedField(queryset=Fuente.objects.all())
class Meta: class Meta:
model = Document 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') read_only_fields = ('id', 'size', 'extension', 'created_at', 'updated_at', 'pedimento_numero')
def get_pedimento_numero(self, obj): def get_pedimento_numero(self, obj):
@@ -26,6 +27,12 @@ class DocumentSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("Se requiere un archivo para subir") raise serializers.ValidationError("Se requiere un archivo para subir")
return value 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 FuenteSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Fuente model = Fuente

View File

@@ -4,7 +4,16 @@ from rest_framework.routers import DefaultRouter
# import necessary viewsets # import necessary viewsets
# from .views import YourViewSet # Import your viewsets here # from .views import YourViewSet # Import your viewsets here
from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView, ExpedienteZipDownloadView, MultiPedimentoZipDownloadView from .views import (DocumentViewSet
, ProtectedDocumentDownloadView
, BulkDownloadZipView
, GetFuenteView
, DocumentTypeView
, ExpedienteZipDownloadView
, MultiPedimentoZipDownloadView
, PedimentoDocumentViewSet)
# Create a router and register your viewsets with it # Create a router and register your viewsets with it
router = DefaultRouter() router = DefaultRouter()
@@ -25,5 +34,6 @@ urlpatterns = [
path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'), path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'),
path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'), path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'),
path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'), path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'),
path('pedimento-documents/', PedimentoDocumentViewSet.as_view({'get': 'list'}), name='pedimento-document-list'),
path('', include(router.urls)), path('', include(router.urls)),
] ]

View File

@@ -63,7 +63,8 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
pagination_class = CustomPagination pagination_class = CustomPagination
serializer_class = DocumentSerializer serializer_class = DocumentSerializer
# Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado) # Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado)
filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento'] filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento', 'created_at']
# filterset_fields = ['extension', 'size', 'pedimento', 'pedimento__pedimento']
# Puedes filtrar por pedimento usando: /api/record/documents/?pedimento=<id> o /api/record/documents/?pedimento__pedimento=<numero> # Puedes filtrar por pedimento usando: /api/record/documents/?pedimento=<id> o /api/record/documents/?pedimento__pedimento=<numero>
# Ejemplo: /api/record/documents/?pedimento_numero=12345678 # Ejemplo: /api/record/documents/?pedimento_numero=12345678
@@ -71,6 +72,33 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
def get_queryset(self): def get_queryset(self):
queryset = self.get_queryset_filtrado_por_organizacion() queryset = self.get_queryset_filtrado_por_organizacion()
modulo_efc = self.request.query_params.get('modulo')
if modulo_efc:
if modulo_efc == 'expedientes-detalle-pedimentos':
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:
# # Puedes agregar lógica personalizada aquí si es necesario
# if document_type == '1':
# queryset = queryset.filter(document_type_id=document_type)
# elif document_type == '2':
# queryset = queryset.filter(document_type_id=document_type)
# else:
# queryset = queryset.filter(document_type_id=document_type)
# else:
# queryset = queryset.filter(document_type_id='11')
fechaCreacion = self.request.query_params.get('created_at__date')
if fechaCreacion:
queryset = queryset.filter(created_at=fechaCreacion)
buscarArchivo = self.request.query_params.get('archivo__icontains')
if buscarArchivo:
queryset = queryset.filter(archivo__icontains=buscarArchivo)
pedimento_numero = self.request.query_params.get('pedimento_numero') pedimento_numero = self.request.query_params.get('pedimento_numero')
if pedimento_numero: if pedimento_numero:
queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero) queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero)
@@ -762,6 +790,50 @@ class MultiPedimentoZipDownloadView(APIView):
return response return response
class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"""
ViewSet for Document model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
model = Document
pagination_class = CustomPagination
serializer_class = DocumentSerializer
# Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado)
# filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento']
filterset_fields = ['extension', 'size', 'pedimento', 'pedimento__pedimento','fuente']
# Puedes filtrar por pedimento usando: /api/record/documents/?pedimento=<id> o /api/record/documents/?pedimento__pedimento=<numero>
# Ejemplo: /api/record/documents/?pedimento_numero=12345678
my_tags = ['Documents']
def get_queryset(self):
queryset = self.get_queryset_filtrado_por_organizacion()
# Tipos de documento permitidos (fijos en código, Pedimento completo y remesas)
TIPOS_PERMITIDOS = ['2', '3'] # <-- Ajusta aquí tus tipos
tipo_documento = self.request.query_params.get('document_type')
if tipo_documento:
queryset = queryset.filter(document_type_id=tipo_documento)
else:
# Filtrar por tipos permitidos
queryset = queryset.filter(document_type_id__in=TIPOS_PERMITIDOS)
buscar_archivo = self.request.query_params.get('archivo__icontains')
if buscar_archivo:
queryset = queryset.filter(archivo__icontains=buscar_archivo)
created_at__date = self.request.query_params.get('created_at__date')
if created_at__date:
queryset = queryset.filter(created_at=created_at__date)
# Filtro adicional por pedimento_numero si se proporciona
pedimento_numero = self.request.query_params.get('pedimento_numero')
if pedimento_numero:
queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero)
return queryset