Compare commits

...

2 Commits

8 changed files with 361 additions and 62 deletions

View File

@@ -34,6 +34,7 @@ class Pedimento(models.Model):
fecha_pago = models.DateField(help_text="Fecha de pago del pedimento", blank=True, null=True)
alerta = models.BooleanField(default=False, help_text="Indica si el pedimento tiene una alerta asociada")
consultar_vucem = models.BooleanField(default=False, help_text="Solo pedimentos originados desde datastage deben consultar VUCEM automáticamente")
contribuyente = models.ForeignKey('Importador', on_delete=models.CASCADE, related_name='pedimentos', help_text="Contribuyente asociado al pedimento", blank=True, null=True)
agente_aduanal = models.CharField(max_length=100, blank=True, null=True, help_text="RFC del agente aduanal")

View File

@@ -27,6 +27,9 @@ def trigger_celery_task_on_create(sender, instance, created, **kwargs):
logger.info("NO es creación de pedimento, no se crea procesamiento.")
return
if not instance.consultar_vucem:
return
def crear_procesamiento():
import logging
logger = logging.getLogger('api.customs.async_operations')

View File

@@ -1,5 +1,4 @@
from celery import shared_task
from django.core.files.base import ContentFile
from django.utils import timezone
import os
import zipfile
@@ -615,8 +614,6 @@ def bulk_upload_record_task(self, organizacion_id, parametros, archivo_paths):
tiene_nomenclatura_especial = True
info_extraida = procesar_archivo_m_con_nomenclatura(file_content, existing_pedimento)
django_file = ContentFile(file_content, name=file_name)
# Buscar documento existente
existing_documents = Document.objects.filter(
pedimento_id=existing_pedimento.id,
@@ -630,51 +627,53 @@ def bulk_upload_record_task(self, organizacion_id, parametros, archivo_paths):
break
if existing_document:
# Actualizar documento existente
# try:
# if existing_document.archivo and os.path.exists(existing_document.archivo.path):
# os.remove(existing_document.archivo.path)
# except (ValueError, OSError):
# pass
# existing_document.archivo = django_file
# existing_document.size = len(file_content)
# existing_document.extension = extension
# existing_document.updated_at = timezone.now()
# existing_document.save()
# doc = Document.objects.get(id=existing_document.id)
# doc.archivo.delete(save=False) # Eliminar el archivo anterior
# doc.delete() # Eliminar el registro para crear uno nuevo (evita problemas con archivos en Django)
updated_pedimentos.append({
"id": str(existing_pedimento.id),
"pedimento_app": existing_pedimento.pedimento_app,
"accion": "Documento actualizado",
"accion": "Documento ya existente, omitido",
"documento": file_name
})
documents_created += 1
else:
# Crear nuevo documento
# Crear registro sin archivo primero
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=existing_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('.')
)
updated_pedimentos.append({
"id": str(existing_pedimento.id),
"pedimento_app": existing_pedimento.pedimento_app,
"accion": "Documento creado",
"documento": file_name
})
from api.utils.storage_service import storage_service
ruta = storage_service.save_document_from_path(
file_path=file_path,
file_name=file_name,
organizacion_id=organizacion.id,
pedimento_app=existing_pedimento.pedimento_app,
metadata={
'pedimento_id': str(existing_pedimento.id),
'document_id': str(document.id),
'source': 'bulk_upload_async'
}
)
documents_created += 1
if ruta:
document.archivo = ruta
document.save()
documents_created += 1
updated_pedimentos.append({
"id": str(existing_pedimento.id),
"pedimento_app": existing_pedimento.pedimento_app,
"accion": "Documento creado",
"documento": file_name
})
else:
document.delete()
failed_records.append({
"file": relative_path,
"archivo_original": folder_name + '.zip',
"error": f"Error al guardar {file_name} en almacenamiento"
})
except Exception as e:
failed_records.append({

View File

@@ -563,11 +563,14 @@ def process_all_organizations():
"""
Envía una tarea por organización activa a la cola org_processing.
"""
active_orgs = Organizacion.objects.filter(is_active=True, is_verified=True)
active_orgs = Organizacion.objects.filter(
is_active=True,
is_verified=True,
apply_auto_download=True,
)
for org in active_orgs:
process_organization_batch.apply_async(
args=[org.id],
args=[str(org.id)],
queue='org_processing'
)
return f"Dispatched {active_orgs.count()} organizations"

View File

@@ -297,6 +297,7 @@ def procesar_archivo_asc_task(datastage_id, user_organizacion_id, asc_name):
"importe_pedimento": data.get('importe_pedimento', 0.0),
"existe_expediente": data.get('existe_expediente', False),
"remesas": data.get('remesas', False),
"consultar_vucem": True,
}
try:
Pedimento.objects.create(**pedimento_data)

View File

@@ -42,6 +42,7 @@ class Organizacion(models.Model):
is_active = models.BooleanField(default=True)
is_verified = models.BooleanField(default=False)
apply_auto_download = models.BooleanField(default=False)
inicio = models.DateField(null=True, blank=True)
vencimiento = models.DateField(null=True, blank=True)

View File

@@ -1278,15 +1278,21 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
status=status.HTTP_403_FORBIDDEN
)
# Usar tipo de documento por defecto siempre
document_type, created = DocumentType.objects.get_or_create(
nombre="Documento General",
defaults={'descripcion': "Documento general sin tipo específico"}
)
if created:
print(f"✅ DocumentType creado: {document_type.nombre} (ID: {document_type.id})")
# Usar tipo de documento indicado o "Documento General" por defecto
document_type_id_param = request.data.get('document_type_id')
if document_type_id_param:
try:
document_type = DocumentType.objects.get(id=int(document_type_id_param))
except (DocumentType.DoesNotExist, ValueError):
return Response(
{"error": f"Tipo de documento con ID '{document_type_id_param}' no encontrado"},
status=status.HTTP_400_BAD_REQUEST
)
else:
print(f"♻️ DocumentType existente: {document_type.nombre} (ID: {document_type.id})")
document_type, _ = DocumentType.objects.get_or_create(
nombre="Documento General",
defaults={'descripcion': "Documento general sin tipo específico"}
)
uploaded_documents = []
failed_files = []
@@ -1371,6 +1377,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
existing_doc.archivo = ruta
existing_doc.size = file.size
existing_doc.extension = extension
existing_doc.document_type = document_type
existing_doc.save()
else:
raise Exception(f"Error al guardar archivo: {file.name}")
@@ -1406,7 +1413,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"filename": file.name,
"size": file.size,
"extension": extension,
"document_type": document_type.nombre
"document_type": document.document_type.nombre if document.document_type else None
})
except Exception as e:
@@ -1750,6 +1757,265 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
return Response(response_data, status=response_status)
@action(detail=False, methods=['post'], url_path='create-vu-record', parser_classes=[MultiPartParser])
def create_vu_record(self, request):
"""
Crea un registro (Partida/Cove/EDocument) en su tabla correspondiente
y sube sus archivos con la nomenclatura VU.
FormData:
- pedimento_id : UUID del pedimento
- tab_seccion : 'partida' | 'cove' | 'edoc'
- numero : número del registro a crear
- files : archivos (nombre con flag de sección: .xml.general, .pdf.acuse, etc.)
"""
pedimento_id = request.data.get('pedimento_id')
tab_seccion = request.data.get('tab_seccion')
numero = request.data.get('numero', '').strip()
files = request.FILES.getlist('files')
if not pedimento_id:
return Response({"error": "Se requiere 'pedimento_id'"}, status=status.HTTP_400_BAD_REQUEST)
if tab_seccion not in ('partida', 'cove', 'edoc'):
return Response({"error": "tab_seccion debe ser 'partida', 'cove' o 'edoc'"}, status=status.HTTP_400_BAD_REQUEST)
if not numero:
return Response({"error": "Se requiere 'numero'"}, status=status.HTTP_400_BAD_REQUEST)
if not files:
return Response({"error": "Se requiere al menos un archivo"}, status=status.HTTP_400_BAD_REQUEST)
if not request.user.is_authenticated:
return Response({"error": "Usuario no autenticado"}, status=status.HTTP_401_UNAUTHORIZED)
from api.customs.models import Pedimento as PedimentoModel, Partida, Cove, EDocument
try:
pedimento = PedimentoModel.objects.get(id=pedimento_id)
except PedimentoModel.DoesNotExist:
return Response({"error": "Pedimento no encontrado"}, status=status.HTTP_404_NOT_FOUND)
organizacion = pedimento.organizacion
if not request.user.is_superuser:
if not hasattr(request.user, 'organizacion') or request.user.organizacion != organizacion:
return Response({"error": "Sin permisos para este pedimento"}, status=status.HTTP_403_FORBIDDEN)
# Validar número entero para partida
numero_int = None
if tab_seccion == 'partida':
try:
numero_int = int(numero)
except ValueError:
return Response({"error": "El número de partida debe ser un entero"}, status=status.HTTP_400_BAD_REQUEST)
uploaded_documents = []
failed_files = []
errors = []
total_space_used = 0
expediente_obj = None
try:
with transaction.atomic():
uso = UsoAlmacenamiento.objects.select_for_update().get_or_create(
organizacion=organizacion,
defaults={'espacio_utilizado': 0}
)[0]
max_bytes = organizacion.licencia.almacenamiento * 1024 ** 3
total_files_size = sum(f.size for f in files)
if uso.espacio_utilizado + total_files_size > max_bytes:
espacio_faltante = (uso.espacio_utilizado + total_files_size) - max_bytes
return Response({
"error": "Espacio de almacenamiento insuficiente",
"espacio_faltante_gb": round(espacio_faltante / (1024 ** 3), 2),
}, status=status.HTTP_400_BAD_REQUEST)
# Verificar unicidad y crear registro
if tab_seccion == 'partida':
if Partida.objects.filter(pedimento=pedimento, numero_partida=numero_int).exists():
return Response(
{"error": f"La partida {numero} ya existe para este pedimento"},
status=status.HTTP_409_CONFLICT
)
expediente_obj = Partida.objects.create(
pedimento=pedimento,
organizacion=organizacion,
numero_partida=numero_int
)
elif tab_seccion == 'cove':
if Cove.objects.filter(pedimento=pedimento, numero_cove=numero).exists():
return Response(
{"error": f"El COVE {numero} ya existe para este pedimento"},
status=status.HTTP_409_CONFLICT
)
expediente_obj = Cove.objects.create(
pedimento=pedimento,
organizacion=organizacion,
numero_cove=numero
)
elif tab_seccion == 'edoc':
if EDocument.objects.filter(pedimento=pedimento, numero_edocument=numero).exists():
return Response(
{"error": f"El EDocument {numero} ya existe para este pedimento"},
status=status.HTTP_409_CONFLICT
)
expediente_obj = EDocument.objects.create(
pedimento=pedimento,
organizacion=organizacion,
numero_edocument=numero
)
espacio_usado_temp = uso.espacio_utilizado
uploaded_secciones = set()
for file in files:
try:
if not file.name:
failed_files.append("archivo_sin_nombre")
errors.append("Archivo sin nombre detectado")
continue
filename = file.name
if '.' in filename:
base = '.'.join(filename.split('.')[:-1])
secciones = filename.split('.')[-1]
else:
base = filename
secciones = ''
file.name = base
extension = file.name.split('.')[-1].lower() if '.' in file.name else ''
if tab_seccion == 'partida':
nuevo_nombre = f"vu_PT_{pedimento.pedimento_app}_{numero}.{extension}"
document_type, _ = DocumentType.objects.get_or_create(
nombre="Pedimento Partida",
defaults={'descripcion': "Tag para saber que el archivo guarda una partida"}
)
elif tab_seccion == 'cove':
if secciones == 'acuse':
nuevo_nombre = f"vu_AC_COVE_{pedimento.pedimento_app}_{numero}.{extension}"
document_type, _ = DocumentType.objects.get_or_create(
nombre="Acuse Cove",
defaults={'descripcion': "Tag para saber que el archivo guarda un acuse de cove"}
)
else:
nuevo_nombre = f"vu_COVE_{pedimento.pedimento_app}_{numero}.{extension}"
document_type, _ = DocumentType.objects.get_or_create(
nombre="Cove",
defaults={'descripcion': "Tag para saber que el archivo guarda un cove"}
)
elif tab_seccion == 'edoc':
if secciones == 'acuse':
nuevo_nombre = f"vu_AC_{pedimento.pedimento_app}_{numero}.{extension}"
document_type, _ = DocumentType.objects.get_or_create(
nombre="Pedimento Acuse",
defaults={'descripcion': "Tag para saber que el documento es un Acuse"}
)
else:
nuevo_nombre = f"vu_ED_{pedimento.pedimento_app}_{numero}.{extension}"
document_type, _ = DocumentType.objects.get_or_create(
nombre="Pedimento EDocument",
defaults={'descripcion': "Tag para saber que el documento es un EDocument"}
)
file.name = nuevo_nombre
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento_id,
document_type=document_type,
size=file.size,
extension=extension
)
ruta = storage_service.save_document(
file=file,
organizacion_id=organizacion.id,
pedimento_app=pedimento.pedimento_app,
metadata={'source': 'create_vu_record'}
)
if ruta:
document.archivo = ruta
document.save()
else:
document.delete()
raise Exception(f"Error al guardar archivo: {file.name}")
espacio_usado_temp += file.size
total_space_used += file.size
uploaded_secciones.add(secciones)
uploaded_documents.append({
"id": str(document.id),
"filename": file.name,
"size": file.size,
"extension": extension,
"document_type": document_type.nombre
})
except Exception as e:
failed_files.append(file.name)
errors.append(f"Error al procesar {file.name}: {str(e)}")
continue
# Actualizar flags de descarga según secciones subidas exitosamente
if tab_seccion == 'partida':
if uploaded_secciones:
expediente_obj.descargado = True
expediente_obj.save(update_fields=['descargado'])
elif tab_seccion == 'cove':
update_fields = []
if 'general' in uploaded_secciones:
expediente_obj.cove_descargado = True
update_fields.append('cove_descargado')
if 'acuse' in uploaded_secciones:
expediente_obj.acuse_cove_descargado = True
update_fields.append('acuse_cove_descargado')
if update_fields:
expediente_obj.save(update_fields=update_fields)
elif tab_seccion == 'edoc':
update_fields = []
if 'general' in uploaded_secciones:
expediente_obj.edocument_descargado = True
update_fields.append('edocument_descargado')
if 'acuse' in uploaded_secciones:
expediente_obj.acuse_descargado = True
update_fields.append('acuse_descargado')
if update_fields:
expediente_obj.save(update_fields=update_fields)
uso.espacio_utilizado = espacio_usado_temp
uso.save()
except Exception as e:
return Response(
{"error": f"Error durante el procesamiento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
space_used_mb = round(total_space_used / (1024 * 1024), 2)
response_data = {
"uploaded_count": len(uploaded_documents),
"uploaded_documents": uploaded_documents,
"space_used_mb": space_used_mb,
"pedimento_id": str(pedimento_id),
"expediente_id": str(expediente_obj.id),
"tab_seccion": tab_seccion,
"numero": numero,
}
if failed_files:
response_data.update({
"message": f"Registro creado pero algunos archivos fallaron",
"failed_files": failed_files,
"errors": errors
})
response_status = status.HTTP_207_MULTI_STATUS
else:
response_data["message"] = f"{tab_seccion.capitalize()} {numero} creado exitosamente"
response_status = status.HTTP_201_CREATED
return Response(response_data, status=response_status)
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = DocumentSerializer

View File

@@ -3,7 +3,6 @@ import tempfile
from api.utils.storage_service import storage_service
from celery import shared_task
from api.organization.models import Organizacion
from django.core.files.base import ContentFile
from django.utils import timezone
from api.reports.models import ReportDocument
from api.customs.models import Pedimento, Cove, EDocument, Partida
@@ -127,8 +126,8 @@ def generate_report_document(report_id):
@shared_task
def generate_report_control_pedimento(report_id):
report = None
try:
report = ReportDocument.objects.get(id=report_id)
report.status = 'processing'
report.save(update_fields=['status'])
@@ -222,8 +221,9 @@ def generate_report_control_pedimento(report_id):
# 4. GENERAR CSV CON DETALLES
filename = f"report_{report.id}_{timezone.now().strftime('%Y%m%d%H%M%S')}.csv"
file_path = os.path.join(settings.MEDIA_ROOT, 'reports', filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', encoding='utf-8', newline='') as tmp:
tmp_path = tmp.name
todas_las_filas = []
@@ -278,7 +278,7 @@ def generate_report_control_pedimento(report_id):
todas_las_filas.append(fila)
# 5. ESCRIBIR ARCHIVO CSV
with open(file_path, 'w', newline='', encoding='utf-8') as f:
with open(tmp_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# SECCIÓN DE TOTALES
@@ -308,15 +308,40 @@ def generate_report_control_pedimento(report_id):
writer.writerow(fila)
with open(file_path, 'rb') as f:
report.file.save(filename, ContentFile(f.read()), save=True)
with open(tmp_path, 'rb') as f:
file_content = f.read()
uploaded_file = SimpleUploadedFile(
name=filename,
content=file_content,
content_type='text/csv'
)
ruta = storage_service.save_report(
file=uploaded_file,
organizacion_id=filters.get('organizacion_id'),
metadata={
'report_id': str(report.id),
'report_type': 'control_pedimento',
'user_id': str(report.user.id) if report.user else None
}
)
os.unlink(tmp_path)
if ruta:
report.file = ruta
report.status = 'ready'
else:
report.status = 'error'
report.error_message = 'Error al guardar el archivo en storage'
report.status = 'ready'
report.finished_at = timezone.now()
report.save(update_fields=['status', 'file', 'finished_at'])
report.save(update_fields=['status', 'file', 'finished_at', 'error_message'])
except Exception as e:
report.status = 'error'
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])
if report:
report.status = 'error'
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])