1 Commits

Author SHA1 Message Date
Dulce
f8379807f8 agregar expedientes mediante celery 2026-01-19 09:46:35 -07:00
19 changed files with 760 additions and 1439 deletions

View File

@@ -6,5 +6,4 @@ class CustomsConfig(AppConfig):
name = 'api.customs' name = 'api.customs'
def ready(self): def ready(self):
# corregir el import aqui import api.customs.signals
import api.customs.signals.procesamiento

View File

@@ -36,8 +36,7 @@ class Command(BaseCommand):
if organizacion_id: if organizacion_id:
if procesamiento: if procesamiento:
# microservice_v2.ejecutar_procesamiento_por_organizacion(organizacion_id, procesamiento) microservice_v2.ejecutar_procesamiento_por_organizacion(organizacion_id, procesamiento)
microservice_v2.ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento)
self.stdout.write(self.style.SUCCESS(f'Se ejecutó el procesamiento {procesamiento} para la organización {organizacion_id}.')) self.stdout.write(self.style.SUCCESS(f'Se ejecutó el procesamiento {procesamiento} para la organización {organizacion_id}.'))
else: else:
microservice_v2.ejecutar_todos_por_organizacion(organizacion_id) microservice_v2.ejecutar_todos_por_organizacion(organizacion_id)

View File

@@ -1,5 +1,6 @@
import uuid import uuid
from django.db import models from django.db import models
from django.contrib.auth import get_user_model
# Create your models here. # Create your models here.
@@ -61,7 +62,7 @@ class Pedimento(models.Model):
db_table = 'pedimento' db_table = 'pedimento'
ordering = ['pedimento'] ordering = ['pedimento']
unique_together = [ unique_together = [
# ['organizacion', 'pedimento'], ['organizacion', 'pedimento'],
['organizacion', 'pedimento_app'] ['organizacion', 'pedimento_app']
] ]
@@ -211,3 +212,44 @@ class Importador(models.Model):
def __str__(self): def __str__(self):
return f"{self.rfc} - {self.nombre}" 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']

View File

@@ -3,7 +3,7 @@ from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from time import sleep from time import sleep
from api.customs.models import EstadoDeProcesamiento, Pedimento, ProcesamientoPedimento, Cove, EDocument from api.customs.models import Pedimento, ProcesamientoPedimento, Cove, EDocument
from api.customs.tasks.internal_services import ( from api.customs.tasks.internal_services import (
crear_procesamiento_remesa, crear_procesamiento_remesa,
crear_procesamiento_partida, crear_procesamiento_partida,
@@ -20,49 +20,8 @@ from api.customs.tasks.microservice import (
@receiver(post_save, sender=Pedimento) @receiver(post_save, sender=Pedimento)
def trigger_celery_task_on_create(sender, instance, created, **kwargs): def trigger_celery_task_on_create(sender, instance, created, **kwargs):
if created:
if not created:
import logging
logger = logging.getLogger('api.customs.async_operations')
logger.info("NO es creación de pedimento, no se crea procesamiento.")
return
def crear_procesamiento():
import logging
logger = logging.getLogger('api.customs.async_operations')
logger.info(f"Pedimento confirmado en BD: {instance.id}, creando procesamiento...")
try:
estado, _ = EstadoDeProcesamiento.objects.get_or_create(
estado='En Espera'
)
except Exception:
estado = EstadoDeProcesamiento.objects.first()
try:
ProcesamientoPedimento.objects.get_or_create(
pedimento=instance,
organizacion=instance.organizacion,
defaults={
'estado': estado,
'servicio_id': 3,
'tipo_procesamiento_id': 2,
}
)
except Exception as e:
logger.exception(
f"No se pudo crear ProcesamientoPedimento "
f"para pedimento {instance.id}: {e}"
)
# Disparar la tarea asíncrona existente
try:
procesar_pedimento_completo_individual.apply_async(args=[instance.id, instance.organizacion.id]) procesar_pedimento_completo_individual.apply_async(args=[instance.id, instance.organizacion.id])
except Exception as e:
logger.exception(f"Error al encolar procesar_pedimento_completo_individual: {e}")
transaction.on_commit(crear_procesamiento)
@receiver(post_save, sender=Pedimento) @receiver(post_save, sender=Pedimento)
def trigger_celery_task_on_update(sender, instance, created,**kwargs): def trigger_celery_task_on_update(sender, instance, created,**kwargs):

View File

@@ -1,2 +1,3 @@
from .microservice import * from .microservice import *
from .internal_services import * from .internal_services import *
from .bulk_pedimentos import *

View File

@@ -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)}")

View File

@@ -11,9 +11,6 @@ from datetime import datetime
# =================== # ===================
@shared_task @shared_task
def procesar_pedimento_completo_individual(pedimento_id, organizacion_id): def procesar_pedimento_completo_individual(pedimento_id, organizacion_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
logger.info(f"Pedimento a monitorear: {pedimento_id}, org:: {organizacion_id}, verificando servicios a crear...")
response = requests.post( response = requests.post(
f"{SERVICE_API_URL}/async/services/pedimento_completo", f"{SERVICE_API_URL}/async/services/pedimento_completo",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)} json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}

View File

@@ -222,15 +222,14 @@ def procesar_pedimentos_completos(organizacion_id):
pedimento_dict = pedimento_to_dict(pedimento) pedimento_dict = pedimento_to_dict(pedimento)
credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first() credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first()
if not credenciales:
print(f"No se encontraron credenciales para el pedimento {pedimento.pedimento_app}")
continue
credenciales_dict = credenciales_to_dict(credenciales) credenciales_dict = credenciales_to_dict(credenciales)
payload = { payload = {
"pedimento": pedimento_dict, "pedimento": pedimento_dict,
"credencial": credenciales_dict "credencial": credenciales_dict
} }
response = requests.post( response = requests.post(
f"{SERVICE_API_URL_V2}/services/pedimento_completo", f"{SERVICE_API_URL_V2}/services/pedimento_completo",
data=json.dumps(payload), data=json.dumps(payload),
@@ -429,34 +428,6 @@ def documentos_con_errores(organizacion_id):
# Aquí puedes agregar lógica adicional para manejar documentos con errores # Aquí puedes agregar lógica adicional para manejar documentos con errores
# como enviar notificaciones, registrar en un log, etc. # como enviar notificaciones, registrar en un log, etc.
@shared_task
def procesar_procesamiento_pedimento(organizacion_id):
# print("Creando procesamientos de pedimentos para organización:", organizacion_id)
pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id)
# pedimentos = Pedimento.objects.filter(id='1c061182-ac68-45b0-b3d7-35bf2264982b')
if not pedimentos.exists():
print("No se encontraron pedimentos para la organización:", organizacion_id)
return
for pedimento in pedimentos:
if not pedimento.documents.filter(document_type=2).exists(): # Tipo 2: Pedimento Completo
procesamiento_pedimento = ProcesamientoPedimento.objects.filter(
pedimento_id=pedimento.id,
servicio_id=3, # servicio 3: Pedimento Completo
)
if not procesamiento_pedimento.exists():
ProcesamientoPedimento.objects.create(
pedimento_id=pedimento.id
, organizacion_id=pedimento.organizacion_id
, estado_id =1
, servicio_id=3
, tipo_procesamiento_id=2) # servicio 3: Pedimento Completo
# print("Procesamiento creado para pedimento:", pedimento.pedimento_app)
procesar_pedimentos_completos.delay(organizacion_id)
def ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento): def ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento):
if procesamiento == 'coves': if procesamiento == 'coves':
@@ -473,11 +444,9 @@ def ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento):
procesar_pedimentos_completos.delay(organizacion_id) procesar_pedimentos_completos.delay(organizacion_id)
elif procesamiento == 'remesas': elif procesamiento == 'remesas':
procesar_remesas.delay(organizacion_id) procesar_remesas.delay(organizacion_id)
elif procesamiento == 'procesamiento_pedimento':
procesar_procesamiento_pedimento.delay(organizacion_id)
else: else:
# Procesamiento no reconocido # Procesamiento no reconocido
# print(f"Procesamiento no reconocido: {procesamiento}")
pass pass
def ejecutar_todos_por_organizacion(organizacion_id): def ejecutar_todos_por_organizacion(organizacion_id):
@@ -490,5 +459,3 @@ def ejecutar_todos_por_organizacion(organizacion_id):
procesar_remesas.delay(organizacion_id) procesar_remesas.delay(organizacion_id)

View File

@@ -10,8 +10,7 @@ from .views import (
ViewSetEDocument, ViewSetEDocument,
ViewSetCove, ViewSetCove,
ImportadorViewSet, ImportadorViewSet,
PartidaViewSet, PartidaViewSet
EjecutarComandoView
) )
# from .views import YourViewSet # Import your viewsets here # from .views import YourViewSet # Import your viewsets here
@@ -96,7 +95,4 @@ urlpatterns = [
path('auditor/obtener-respuesta/acuse-cove-vu/', auditor_obtener_respuesta_acuse_cove_vu, name='obtener-respuesta-acuse-cove-vu'), path('auditor/obtener-respuesta/acuse-cove-vu/', auditor_obtener_respuesta_acuse_cove_vu, name='obtener-respuesta-acuse-cove-vu'),
path('auditor/obtener-peticion/edocument-vu/', auditor_obtener_peticion_edocument_vu, name='obtener-peticion-edocument-vu'), path('auditor/obtener-peticion/edocument-vu/', auditor_obtener_peticion_edocument_vu, name='obtener-peticion-edocument-vu'),
path('auditor/obtener-respuesta/edocument-vu/', auditor_obtener_respuesta_edocument_vu, name='obtener-respuesta-edocument-vu'), path('auditor/obtener-respuesta/edocument-vu/', auditor_obtener_respuesta_edocument_vu, name='obtener-respuesta-edocument-vu'),
path('procesamientopedimentos-ejecutar-comando/', EjecutarComandoView.as_view(), name='procesamientopedimentos-ejecutar-comando'),
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -667,36 +667,28 @@ def auditar_peticion_respuesta_pedimento_completo(request):
pedimento_app = pedimento.pedimento_app pedimento_app = pedimento.pedimento_app
tipo_documento_peticion = None tipo_documento_peticion = None
tipo_documento_respuesta = None tipo_documento_respuesta = None
vista = 'desconocido'
if vista_auditar == 'pc': if vista_auditar == 'pc':
tipo_documento_peticion = 13 tipo_documento_peticion = 13
tipo_documento_respuesta = 14 tipo_documento_respuesta = 14
vista = 'Pedimento Completo'
elif vista_auditar == 'rm': elif vista_auditar == 'rm':
tipo_documento_peticion = 15 tipo_documento_peticion = 15
tipo_documento_respuesta = 16 tipo_documento_respuesta = 16
vista = 'Remesa'
elif vista_auditar == 'pt': elif vista_auditar == 'pt':
tipo_documento_peticion = 17 tipo_documento_peticion = 17
tipo_documento_respuesta = 18 tipo_documento_respuesta = 18
vista = 'Partidas'
elif vista_auditar == 'cove': elif vista_auditar == 'cove':
tipo_documento_peticion = 19 tipo_documento_peticion = 19
tipo_documento_respuesta = 20 tipo_documento_respuesta = 20
vista = 'COVEs'
elif vista_auditar == 'edoc': elif vista_auditar == 'edoc':
tipo_documento_peticion = 21 tipo_documento_peticion = 21
tipo_documento_respuesta = 22 tipo_documento_respuesta = 22
vista = 'Edocuments'
elif vista_auditar == 'ac_cove': elif vista_auditar == 'ac_cove':
tipo_documento_peticion = 23 tipo_documento_peticion = 23
tipo_documento_respuesta = 24 tipo_documento_respuesta = 24
vista = 'Acuses COVEs'
elif vista_auditar == 'ac': elif vista_auditar == 'ac':
tipo_documento_peticion = 25 tipo_documento_peticion = 25
tipo_documento_respuesta = 26 tipo_documento_respuesta = 26
vista = 'Acuses'
if not tipo_documento_peticion and not tipo_documento_respuesta: if not tipo_documento_peticion and not tipo_documento_respuesta:
return Response( return Response(
@@ -720,7 +712,7 @@ def auditar_peticion_respuesta_pedimento_completo(request):
if not documentos_peticion and not documentos_respuesta: if not documentos_peticion and not documentos_respuesta:
return Response( return Response(
{'error': f'Registro de documentos de petición y respuesta de {vista} no encontrado(s)'}, {'error': 'Registro de documentos de petición y respuesta de partidas no encontrado'},
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND
) )

View File

@@ -61,35 +61,18 @@ class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
if self.request.user.is_superuser: if self.request.user.is_superuser:
# Permitir que el superusuario cree sin organización o la especifique # Permitir que el superusuario cree sin organización o la especifique
datastage = serializer.save() serializer.save()
self._trigger_processing(datastage)
return return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists(): if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
if not organizacion: if not organizacion:
datastage = serializer.save(organizacion=self.request.user.organizacion) serializer.save(organizacion=self.request.user.organizacion)
else: else:
datastage = serializer.save() serializer.save()
self._trigger_processing(datastage)
return return
raise ValueError("No cuentas con los permisos necesarios para crear un DataStage") raise ValueError("No cuentas con los permisos necesarios para crear un DataStage")
def _trigger_processing(self, datastage):
"""
Método helper para disparar el procesamiento.
"""
from api.datastage.tasks import procesar_datastage_task
user_organizacion = getattr(self.request.user, 'organizacion', None)
user_organizacion_id = user_organizacion.id if user_organizacion else None
datastage.procesado = True
datastage.save()
task = procesar_datastage_task.delay(datastage.id, user_organizacion_id)
def perform_update(self, serializer): def perform_update(self, serializer):
""" """
Override to ensure organization is set on update. Override to ensure organization is set on update.
@@ -130,7 +113,6 @@ class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
""" """
Endpoint para procesar el DataStage de forma asíncrona usando Celery. Endpoint para procesar el DataStage de forma asíncrona usando Celery.
""" """
# ojo aqui
from api.datastage.tasks import procesar_datastage_task from api.datastage.tasks import procesar_datastage_task
datastage = self.get_object() datastage = self.get_object()
user_organizacion = getattr(self.request.user, 'organizacion', None) user_organizacion = getattr(self.request.user, 'organizacion', None)

View File

@@ -15,7 +15,6 @@ class Document(models.Model):
extension = models.CharField(max_length=60, blank=True, null=True) extension = models.CharField(max_length=60, blank=True, null=True)
size = models.PositiveIntegerField() size = models.PositiveIntegerField()
fuente = models.ForeignKey('Fuente', on_delete=models.CASCADE, related_name='documents', blank=True, null=True) fuente = models.ForeignKey('Fuente', on_delete=models.CASCADE, related_name='documents', blank=True, null=True)
vu = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -23,13 +22,6 @@ class Document(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
is_new = self._state.adding is_new = self._state.adding
# Calcular automáticamente el campo vu
if self.document_type_id:
# rango de IDs que indican documentos VU
self.vu = 13 <= self.document_type_id <= 26
else:
self.vu = False
# Usar get_or_create en lugar de get para manejar el caso cuando no existe # Usar get_or_create en lugar de get para manejar el caso cuando no existe
uso_almacenamiento, created = UsoAlmacenamiento.objects.get_or_create( uso_almacenamiento, created = UsoAlmacenamiento.objects.get_or_create(
organizacion=self.organizacion, organizacion=self.organizacion,

View File

@@ -13,7 +13,7 @@ class DocumentSerializer(serializers.ModelSerializer):
fuente = serializers.PrimaryKeyRelatedField(queryset=Fuente.objects.all()) 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','fuente_nombre','created_at', 'updated_at','vu') 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):

View File

@@ -313,85 +313,6 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
uso.save() uso.save()
instance.delete() instance.delete()
@action(detail=False, methods=['get'], url_path='vu-documentos-errores')
def vu_documentos_errores(self, request):
"""
Endpoint para obtener los documentos VU de error obtenidoss.
Filtra documentos cuyo document_type está en el rango de IDs de documentos VU (13-26).
"""
queryset = self.get_queryset().filter(vu=True)
pedimento_id = request.query_params.get('pedimentoId')
filtroExtension = request.query_params.get('extension')
filtroArchivo = request.query_params.get('archivo__icontains')
filtroFechaCreacion = request.query_params.get('created_at__date')
filtroTipoError = request.query_params.get('tipo_error')
filtroFuente = request.query_params.get('fuente')
document_type_ids = request.query_params.get('document_type_id')
if pedimento_id:
try:
pedimento_obj = Pedimento.objects.get(id=pedimento_id)
queryset = queryset.filter(pedimento_id=pedimento_id)
except Pedimento.DoesNotExist:
return Response(
{"error": "No se encontró el pedimento especificado"},
status=status.HTTP_404_NOT_FOUND
)
if filtroArchivo:
try:
queryset = queryset.filter(archivo__icontains=filtroArchivo)
except ValueError:
return Response(
{"error": "El parámetro Archivo debe ser caracteres válidos"},
status=status.HTTP_400_BAD_REQUEST
)
if filtroExtension:
try:
queryset = queryset.filter(extension__iexact=filtroExtension)
except ValueError:
return Response(
{"error": "El parámetro extension debe ser una extensión válida"},
status=status.HTTP_400_BAD_REQUEST
)
if filtroFechaCreacion:
from django.utils.dateparse import parse_date
fecha = parse_date(filtroFechaCreacion)
if not fecha:
return Response(
{"error": "El parámetro created_at__date debe tener el formato YYYY-MM-DD"},
status=status.HTTP_400_BAD_REQUEST
)
queryset = queryset.filter(created_at__date=fecha)
if filtroTipoError:
try:
ids = [int(i) for i in filtroTipoError.split(',')]
queryset = queryset.filter(document_type_id__in=ids)
except ValueError:
return Response(
{"error": "El parámetro document_type_id debe ser una lista de IDs separados por comas"},
status=status.HTTP_400_BAD_REQUEST
)
if filtroFuente:
try:
ids = [int(i) for i in filtroFuente.split(',')]
queryset = queryset.filter(fuente_id__in=ids)
except ValueError:
return Response(
{"error": "El parámetro fuente debe ser una lista de IDs separados por comas"},
status=status.HTTP_400_BAD_REQUEST
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=['post'], url_path='bulk-delete') @action(detail=False, methods=['post'], url_path='bulk-delete')
def bulk_delete(self, request): def bulk_delete(self, request):
""" """
@@ -504,22 +425,9 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
# Si no existe el registro, no hay nada que actualizar # Si no existe el registro, no hay nada que actualizar
pass pass
# Eliminar los documentos (archivos físicos y registros de BD) # Eliminar los documentos
archivos_eliminados = 0 deleted_count = existing_documents.count()
for doc in existing_documents: existing_documents.delete()
try:
# Eliminar archivo físico
if doc.archivo and doc.archivo.storage.exists(doc.archivo.name):
doc.archivo.delete(save=False) # save=False para no intentar guardar el modelo
# Eliminar registro de la base de datos
doc.delete()
archivos_eliminados += 1
except Exception as e:
errors.append(f"No se pudo eliminar el documento {doc.id}: {str(e)}")
failed_ids.append(str(doc.id))
deleted_count = archivos_eliminados
except Exception as e: except Exception as e:
return Response( return Response(
@@ -529,7 +437,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
# Agregar errores para IDs no encontrados # Agregar errores para IDs no encontrados
if failed_ids: if failed_ids:
errors.extend([f"No se encontró el documento con ID {id} o no pertenece a su organización" for id in failed_ids]) errors = [f"No se encontró el documento con ID {id} o no pertenece a su organización" for id in failed_ids]
# Convertir bytes a MB para la respuesta # Convertir bytes a MB para la respuesta
space_freed_mb = round(total_space_freed / (1024 * 1024), 2) space_freed_mb = round(total_space_freed / (1024 * 1024), 2)
@@ -541,7 +449,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"space_freed_mb": space_freed_mb "space_freed_mb": space_freed_mb
} }
if errors or failed_ids: if failed_ids:
response_data.update({ response_data.update({
"message": "Algunos documentos no pudieron ser eliminados", "message": "Algunos documentos no pudieron ser eliminados",
"failed_ids": failed_ids, "failed_ids": failed_ids,

View File

@@ -8,8 +8,7 @@ class TaskFilter(filters.FilterSet):
timestamp_gte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='gte') timestamp_gte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='gte')
timestamp_lte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='lte') timestamp_lte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='lte')
status = filters.CharFilter(field_name='status') status = filters.CharFilter(field_name='status')
organizacion = filters.UUIDFilter(field_name='organizacion__id') # Cambiado a relación directa
class Meta: class Meta:
model = Task model = Task
fields = ['servicio', 'pedimento_app', 'pedimento', 'timestamp_gte', 'timestamp_lte', 'status', 'organizacion'] fields = ['servicio', 'pedimento_app', 'pedimento', 'timestamp_gte', 'timestamp_lte', 'status']

View File

@@ -4,7 +4,6 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from api.logger.mixins import LoggingMixin from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin
from .models import Task from .models import Task
from .serializers import TaskSerializer from .serializers import TaskSerializer
from .filters import TaskFilter from .filters import TaskFilter
@@ -23,7 +22,7 @@ class TaskPagination(PageNumberPagination):
page_size_query_param = 'page_size' page_size_query_param = 'page_size'
max_page_size = 100 max_page_size = 100
class TaskViewSet(LoggingMixin,viewsets.ModelViewSet,OrganizacionFiltradaMixin): class TaskViewSet(LoggingMixin,viewsets.ModelViewSet):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
queryset = Task.objects.select_related('pedimento', 'servicio').all() queryset = Task.objects.select_related('pedimento', 'servicio').all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
@@ -34,18 +33,3 @@ class TaskViewSet(LoggingMixin,viewsets.ModelViewSet,OrganizacionFiltradaMixin):
ordering = ['-timestamp'] # ordenamiento por defecto, más reciente primero ordering = ['-timestamp'] # ordenamiento por defecto, más reciente primero
my_tags = ['tasks'] my_tags = ['tasks']
def get_queryset(self):
"""
Filtra las tareas según la organización del usuario.
Superusuarios pueden ver todas las tareas.
"""
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
# if user.is_superuser:
# return self.queryset
# # return self.queryset.filter(organizacion_id=user.organizacion.id)
# else:
# return self.queryset.filter(organizacion_id=user.organizacion.id)
return queryset

View File

View File

@@ -1,194 +0,0 @@
# auditoria_xml.py
import xml.etree.ElementTree as ET
from datetime import datetime
def extraer_info_pedimento_xml(xml_content):
"""
Extrae información específica de un XML de pedimento.
"""
try:
# Parsear el XML
root = ET.fromstring(xml_content)
# Buscar el namespace (puede variar)
namespaces = {
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
'ns3': 'http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta'
}
resultado = {}
# Extraer número de operación
num_op = root.find('.//ns2:numeroOperacion', namespaces)
if num_op is not None and num_op.text:
resultado['numero_operacion'] = num_op.text
# Extraer información del pedimento
pedimento_elem = root.find('.//ns2:pedimento', namespaces)
if pedimento_elem is not None:
# Número de pedimento
ped_num = pedimento_elem.find('ns2:pedimento', namespaces)
if ped_num is not None and ped_num.text:
resultado['numero_pedimento'] = ped_num.text
# Número de partidas
partidas = pedimento_elem.find('ns2:partidas', namespaces)
if partidas is not None and partidas.text:
try:
resultado['numero_partidas'] = int(partidas.text)
except (ValueError, TypeError):
pass
# Tipo de operación clave
tipo_op_clave = pedimento_elem.find('.//ns2:tipoOperacion/ns2:clave', namespaces)
if tipo_op_clave is not None and tipo_op_clave.text:
if tipo_op_clave.text.strip() == '1':
resultado['tipo_operacion'] = 'Importacion'
resultado['tipo_operacion_descripcion'] = 'Indica operacion como Importaciones'
elif tipo_op_clave.text.strip() == '2':
resultado['tipo_operacion'] = 'Exportacion'
resultado['tipo_operacion_descripcion'] = 'Indica operacion de exportacion'
# Clave del documento (clave_pedimento)
clave_doc = pedimento_elem.find('.//ns2:claveDocumento/ns2:clave', namespaces)
if clave_doc is not None and clave_doc.text:
resultado['clave_pedimento'] = clave_doc.text.strip()
# Aduana (patente)
aduana = pedimento_elem.find('.//ns2:aduanaEntradaSalida/ns2:clave', namespaces)
if aduana is not None and aduana.text:
resultado['aduana_clave'] = aduana.text.strip()
# Importador/Exportador
importador = pedimento_elem.find('.//ns2:importadorExportador', namespaces)
if importador is not None:
rfc = importador.find('ns2:rfc', namespaces)
if rfc is not None and rfc.text:
resultado['contribuyente_rfc'] = rfc.text.strip()
razon_social = importador.find('ns2:razonSocial', namespaces)
if razon_social is not None and razon_social.text:
resultado['contribuyente_nombre'] = razon_social.text.strip()
# Valor en dólares
valor_dolares = importador.find('ns2:valorDolares', namespaces)
if valor_dolares is not None and valor_dolares.text:
try:
resultado['valor_dolares'] = float(valor_dolares.text)
except (ValueError, TypeError):
pass
# Aduana de despacho
aduana_despacho = importador.find('ns2:aaduanaDespacho/ns2:clave', namespaces)
if aduana_despacho is not None and aduana_despacho.text:
resultado['aduana_despacho'] = aduana_despacho.text.strip()
# Encabezado del pedimento
encabezado = pedimento_elem.find('ns2:encabezado', namespaces)
if encabezado is not None:
# Aduana
aduana = encabezado.find('ns2:aduanaEntradaSalida/ns2:clave', namespaces)
if aduana is not None and aduana.text:
resultado['aduana_clave'] = aduana.text.strip()
# Tipo de cambio
tipo_cambio = encabezado.find('ns2:tipoCambio', namespaces)
if tipo_cambio is not None and tipo_cambio.text:
try:
resultado['tipo_cambio'] = float(tipo_cambio.text)
except (ValueError, TypeError):
pass
# RFC Agente Aduanal
rfc_agente = encabezado.find('ns2:rfcAgenteAduanalSocFactura', namespaces)
if rfc_agente is not None and rfc_agente.text:
resultado['rfc_agente_aduanal'] = rfc_agente.text.strip()
# CURP Apoderado
curp_apoderado = encabezado.find('ns2:curpApoderadomandatario', namespaces)
if curp_apoderado is not None and curp_apoderado.text:
resultado['curp_apoderado'] = curp_apoderado.text.strip()
# Valor Aduanal Total
valor_aduanal = encabezado.find('ns2:valorAduanalTotal', namespaces)
if valor_aduanal is not None and valor_aduanal.text:
try:
resultado['valor_aduanal_total'] = float(valor_aduanal.text)
except (ValueError, TypeError):
pass
# Valor Comercial Total
valor_comercial = encabezado.find('ns2:valorComercialTotal', namespaces)
if valor_comercial is not None and valor_comercial.text:
try:
resultado['valor_comercial_total'] = float(valor_comercial.text)
except (ValueError, TypeError):
pass
# Fechas
fechas = pedimento_elem.findall('.//ns2:fechas', namespaces)
for fecha_elem in fechas:
fecha = fecha_elem.find('ns2:fecha', namespaces)
clave_fecha = fecha_elem.find('ns2:tipo/ns2:clave', namespaces)
if fecha is not None and fecha.text and clave_fecha is not None and clave_fecha.text:
fecha_texto = fecha.text.strip()
clave_fecha_texto = clave_fecha.text.strip()
# Mapeo de claves según especificación
if clave_fecha_texto == '1': # Entrada
resultado['fecha_entrada'] = fecha_texto
elif clave_fecha_texto == '2': # Pago
resultado['fecha_pago'] = fecha_texto
elif clave_fecha_texto == '3': # Extracción
resultado['fecha_extraccion'] = fecha_texto
elif clave_fecha_texto == '5': # Presentación
resultado['fecha_presentacion'] = fecha_texto
elif clave_fecha_texto == '6': # Importación
resultado['fecha_importacion'] = fecha_texto
elif clave_fecha_texto == '7': # Original
resultado['fecha_original'] = fecha_texto
else:
resultado[f'fecha_clave_{clave_fecha_texto}'] = fecha_texto
# Facturas (para COVEs)
facturas = pedimento_elem.findall('.//ns2:facturas', namespaces)
coves_encontrados = []
for factura in facturas:
numero = factura.find('ns2:numero', namespaces)
if numero is not None and numero.text:
coves_encontrados.append(numero.text.strip())
if coves_encontrados:
resultado['coves_en_xml'] = coves_encontrados
# E-Documents
identificadores = pedimento_elem.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
edocs_encontrados = []
for ident in identificadores:
clave = ident.find('claveIdentificador/descripcion', namespaces)
complemento = ident.find('complemento1', namespaces)
if clave is not None and clave.text and 'E_DOCUMENT' in clave.text:
if complemento is not None and complemento.text:
edocs_encontrados.append(complemento.text.strip())
if edocs_encontrados:
resultado['edocuments_en_xml'] = edocs_encontrados
# Verificar si hay error en la respuesta
tiene_error = root.find('.//ns3:tieneError', namespaces)
if tiene_error is not None:
resultado['tiene_error'] = tiene_error.text.lower() == 'true'
return resultado
except ET.ParseError as e:
return {'error_parse': str(e)}
except Exception as e:
return {'error': str(e)}