14 Commits

Author SHA1 Message Date
fa518972ba fix: se agregan nuevos ajustes al filtro y ejecucion de procesos en base al filtro seleccionado. 2026-02-03 16:38:07 -07:00
6299c6f0fe fix: Filtrar procesos por organizacion dependiando del usuario, solo se debe mostrar todos cuando sea superusuario, en caso contrario solo lo que pertenezca al usuario. 2026-02-03 12:01:22 -07:00
67f339bd18 Merge pull request 'fix: se agrega nuevo endpoint para ejecutar el codigo de los comandos creados por kevin para procesdar las consultas a vucem.' (#17) from req--T2025-08-098 into main
Reviewed-on: #17
2026-02-03 17:54:28 +00:00
98331dae8f fix: se agrega nuevo endpoint para ejecutar el codigo de los comandos creados por kevin para procesdar las consultas a vucem. 2026-02-03 10:27:14 -07:00
6eaf6dc6d9 Merge pull request 'fix: se crea comando para ejecutar manualmente todos los pedimentos completos que aun no se hayan descargado por organizacion.' (#16) from fix-ejecucion-manual-proceso-pedimento-completo into main
Reviewed-on: #16
2026-01-30 14:00:57 +00:00
426c2f7065 fix: se crea comando para ejecutar manualmente todos los pedimentos completos que aun no se hayan descargado por organizacion. 2026-01-29 16:55:52 -07:00
86c0dd6d8b Merge pull request 'T2025-09-004' (#15) from T2025-09-004 into main
Reviewed-on: #15
2026-01-29 17:52:36 +00:00
7141e40dc1 fix: se agrega variable para mostrar mensaje correspondiente a las peticiones y respuestas solicitados por el auditor del frontend. 2026-01-29 10:13:53 -07:00
34eb8ed7d9 fix: se crea una nueva pestaña en detalle de expediente para visualizar los archivos de errores devueltos por ventanilla unica. 2026-01-29 07:53:10 -07:00
5e4d498a3c Merge pull request 'fix: Se agrega validacion para no intentar crear de nuevo el pedimento en caso de ya existir. tambien se agrega funcion de obtener del xml de pedimento completo el dato de la aduana completo.' (#14) from fix-T2025-09-007 into main
Reviewed-on: #14
2026-01-27 17:04:23 +00:00
04d19118be fix: Se agrega validacion para no intentar crear de nuevo el pedimento en caso de ya existir. tambien se agrega funcion de obtener del xml de pedimento completo el dato de la aduana completo. 2026-01-26 15:52:11 -07:00
4ccb5fd718 Merge pull request 'only-datastage' (#13) from only-datastage into main
Reviewed-on: #13
2026-01-26 16:20:27 +00:00
Dulce
8e42ae1a43 cambios solo del datastage 2026-01-26 09:07:48 -07:00
Dulce
f98ae6b207 procesar datastage completo 2026-01-26 09:05:22 -07:00
17 changed files with 758 additions and 282 deletions

View File

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

View File

@@ -36,7 +36,8 @@ 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

@@ -61,7 +61,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']
] ]

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 Pedimento, ProcesamientoPedimento, Cove, EDocument from api.customs.models import EstadoDeProcesamiento, 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,8 +20,49 @@ 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

@@ -11,6 +11,9 @@ 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,14 +222,15 @@ 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()
credenciales_dict = credenciales_to_dict(credenciales) if not credenciales:
print(f"No se encontraron credenciales para el pedimento {pedimento.pedimento_app}")
continue
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),
@@ -428,6 +429,34 @@ 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':
@@ -444,9 +473,11 @@ 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):
@@ -459,3 +490,5 @@ def ejecutar_todos_por_organizacion(organizacion_id):
procesar_remesas.delay(organizacion_id) procesar_remesas.delay(organizacion_id)

View File

@@ -10,7 +10,8 @@ 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
@@ -95,4 +96,7 @@ 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'),
] ]

View File

@@ -513,7 +513,8 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
"processed_files": 3, "processed_files": 3,
"summary": "Procesados 3 archivo(s): 5 pedimento(s) creado(s), 15 documento(s) asociado(s)", "summary": "Procesados 3 archivo(s): 5 pedimento(s) creado(s), 15 documento(s) asociado(s)",
"failed_files": [], "failed_files": [],
"errors": [] "errors": [],
"already_existing": [] # Nuevo campo para pedimentos que ya existían
} }
""" """
print(request.data) print(request.data)
@@ -547,6 +548,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
nomenclatura_pattern_sin_anio = re.compile(r'^(\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 = []
already_existing_pedimentos = [] # Para trackear pedimentos que ya existen
failed_files = [] failed_files = []
errors = [] errors = []
documents_created = 0 documents_created = 0
@@ -599,140 +601,41 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
archivo_name = archivo.name.lower() archivo_name = archivo.name.lower()
print(f"Procesando archivo {idx + 1}/{len(archivos)}: {archivo_name}") print(f"Procesando archivo {idx + 1}/{len(archivos)}: {archivo_name}")
# Crear subdirectorio para cada archivo usando el nombre del archivo sin extensión # Extraer nombre base sin extensión para validación
archivo_name_sin_extension = os.path.splitext(archivo.name)[0] 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'): # Validar nomenclatura del nombre del archivo/folder
# Manejar archivo ZIP match = nomenclatura_pattern.match(archivo_name_sin_extension)
print("Es un archivo ZIP") match_sin_anio = nomenclatura_pattern_sin_anio.match(archivo_name_sin_extension)
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(
{"error": f"Archivo ZIP corrupto o inválido: {archivo.name} - {str(e)}"},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
return Response(
{"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(
{"error": f"Error al extraer archivo RAR {archivo.name}: {error_msg}. {help_msg}"},
status=status.HTTP_400_BAD_REQUEST
)
# if not RAR_SUPPORT:
# return Response(
# {"error": "Soporte para archivos RAR no disponible. Instalar rarfile: pip install rarfile"},
# status=status.HTTP_400_BAD_REQUEST
# )
# try:
# with rarfile.RarFile(archivo, 'r') as rar_ref:
# rar_ref.extractall(sub_dir)
# print(f"Archivo RAR {archivo.name} extraído en sub_dir")
# except rarfile.Error as e:
# return Response(
# {"error": f"Error al extraer archivo RAR {archivo.name}: {str(e)}"},
# 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: if not match and not match_sin_anio:
print(f"Nomenclatura inválida: {folder_name}") print(f"Nomenclatura inválida en nombre de archivo: {archivo_name_sin_extension}")
# 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({ failed_files.append({
"file": relative_path, "archivo_original": archivo.name,
"archivo_original": archivo_original, "error": f"Nomenclatura inválida: {archivo_name_sin_extension}. Esperado: anio-aduana-patente-pedimento"
"error": f"Nomenclatura inválida: {folder_name}. Esperado: anio-aduana-patente-pedimento"
}) })
continue continue
# Extraer información del pedimento desde el nombre del archivo
if match: if match:
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 del nombre del archivo - 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: try:
# Convertir año de 2 dígitos a 4 dígitos # Convertir año de 2 dígitos a 4 dígitos
anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio) anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio)
fecha_pago = datetime(anio_completo, 1, 1).date() fecha_pago = datetime(anio_completo, 1, 1).date()
print(f"Fecha de pago calculada: {fecha_pago}") print(f"Fecha de pago calculada: {fecha_pago}")
except ValueError: 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({ failed_files.append({
"file": relative_path, "archivo_original": archivo.name,
"archivo_original": archivo_original,
"error": f"Año inválido: {anio}" "error": f"Año inválido: {anio}"
}) })
continue continue
elif match_sin_anio: 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() aduana, patente, pedimento_num = match_sin_anio.groups()
print(f"Extraído - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") print(f"Extraído del nombre del archivo - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
# Obtener el primer dígito del pedimento # Obtener el primer dígito del pedimento
primer_digito_pedimento = int(pedimento_num[0]) if pedimento_num else 0 primer_digito_pedimento = int(pedimento_num[0]) if pedimento_num else 0
@@ -745,35 +648,93 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Aplicar lógica de comparación # Aplicar lógica de comparación
if año_con_digito <= año_actual: 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 año_final = año_con_digito
else: else:
# Si el año con dígito es mayor al año actual, restar 10
año_final = año_con_digito - 10 año_final = año_con_digito - 10
# Tomar los últimos 2 dígitos del año final # Tomar los últimos 2 dígitos del año final
anio = año_final % 100 anio = año_final % 100
# Crear fecha de pago (primer día del año) # Crear fecha de pago (primer día del año)
fecha_pago = datetime(año_final , 1, 1).date() fecha_pago = datetime(año_final, 1, 1).date()
print(f"Fecha de pago (año actual) calculada: {fecha_pago}") 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}")
# VERIFICAR SI EL PEDIMENTO YA EXISTE ANTES DE PROCESAR EL ARCHIVO
print(f"Buscando pedimento existente con pedimento_app: {pedimento_app} y organización ID: {organizacion.id}") 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( 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}") if existing_pedimento:
print(f"⚠️ Pedimento ya existe: ID {existing_pedimento.id}, pedimento_app: {pedimento_app}")
already_existing_pedimentos.append({
"id": str(existing_pedimento.id),
"pedimento_app": pedimento_app,
"contribuyente": existing_pedimento.contribuyente.rfc if existing_pedimento.contribuyente else None,
"archivo_original": archivo.name
})
# NO procesamos este archivo, pasamos al siguiente
continue
if not existing_pedimento: # Si el pedimento no existe, continuar con el procesamiento normal
print("📝 Pedimento no existe, creando nuevo...") print("📝 Pedimento no existe, continuando con procesamiento...")
# Crear nuevo pedimento
# Crear subdirectorio para cada archivo usando el nombre del archivo sin extensión
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:
failed_files.append({
"archivo_original": archivo.name,
"error": f"Archivo ZIP corrupto o inválido: {str(e)}"
})
continue
except Exception as e:
failed_files.append({
"archivo_original": archivo.name,
"error": f"Error al extraer ZIP: {str(e)}"
})
continue
elif archivo_name.endswith('.rar'):
# Manejar archivo RAR: guardar el archivo en disco y usar helper con fallbacks
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."
failed_files.append({
"archivo_original": archivo.name,
"error": f"Error al extraer archivo RAR: {error_msg}"
})
continue
else:
# Asumir que es un archivo individual
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)
# Ahora crear el pedimento (ya verificamos que no existe)
try: try:
print("🔄 Iniciando creación de pedimento...") print("🔄 Iniciando creación de pedimento...")
@@ -794,9 +755,12 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
pedimento = Pedimento.objects.create( pedimento = Pedimento.objects.create(
organizacion=organizacion, organizacion=organizacion,
contribuyente=importador, contribuyente=importador,
pedimento=int(pedimento_num), # pedimento=int(pedimento_num),
aduana=int(aduana), pedimento=pedimento_num,
patente=int(patente), aduana=aduana,
# aduana=int(aduana),
# patente=int(patente),
patente=patente,
fecha_pago=fecha_pago, fecha_pago=fecha_pago,
pedimento_app=pedimento_app, pedimento_app=pedimento_app,
agente_aduanal=f"Agente {patente}", # Valor por defecto agente_aduanal=f"Agente {patente}", # Valor por defecto
@@ -809,36 +773,55 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
"id": str(pedimento.id), "id": str(pedimento.id),
"pedimento_app": pedimento_app, "pedimento_app": pedimento_app,
"contribuyente": importador.rfc, "contribuyente": importador.rfc,
"contribuyente_nombre": importador.nombre "contribuyente_nombre": importador.nombre,
"archivo_original": archivo.name
}) })
except Exception as e: except Exception as e:
print(f"❌ Error al crear pedimento: {str(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({ failed_files.append({
"file": relative_path, "archivo_original": archivo.name,
"archivo_original": archivo_original,
"error": f"Error al crear pedimento: {str(e)}" "error": f"Error al crear pedimento: {str(e)}"
}) })
continue continue
else:
pedimento = existing_pedimento # Procesar documentos dentro del directorio
print("Procesando documentos del directorio...")
for root, dirs, files in os.walk(sub_dir):
for file_name in files:
file_path = os.path.join(root, file_name)
print(f"Procesando documento: {file_name}")
try: try:
# Leer el archivo desde el directorio temporal # Leer el archivo desde el directorio temporal
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
file_content = f.read() file_content = f.read()
from api.utils.helpers import extraer_info_pedimento_xml
# Extraer info del pedimento desde XML si es aplicable
if file_name.lower().endswith('.xml'):
try:
xml_info = extraer_info_pedimento_xml(file_content)
if xml_info:
if 'numero_operacion' in xml_info:
if 'numero_pedimento' in xml_info:
if xml_info['numero_pedimento'] == str(pedimento.pedimento):
Pedimento.objects.filter(id=pedimento.id).update(
aduana=xml_info.get('aduana_clave', pedimento.aduana)
)
print(f"Información extraída del XML: {xml_info}")
except Exception as e:
print(f"No se pudo extraer información del XML {file_name}: {str(e)}")
# Obtener información del archivo # Obtener información del archivo
extension = os.path.splitext(file_name)[1].lower().lstrip('.') extension = os.path.splitext(file_name)[1].lower().lstrip('.')
# Buscar todos los documentos existentes para este pedimento # Buscar si ya existe un documento con el mismo nombre para este pedimento
existing_documents = Document.objects.filter( existing_documents = Document.objects.filter(
pedimento_id=pedimento.id, pedimento_id=pedimento.id,
organizacion=organizacion organizacion=organizacion
) )
# Buscar si ya existe un documento con el mismo nombre base
existing_document = None existing_document = None
for doc in existing_documents: for doc in existing_documents:
if is_same_document(doc, file_name): if is_same_document(doc, file_name):
@@ -863,6 +846,8 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
existing_document.extension = extension existing_document.extension = extension
existing_document.updated_at = timezone.now() # Si tienes este campo existing_document.updated_at = timezone.now() # Si tienes este campo
existing_document.save() existing_document.save()
documents_created += 1
print(f"📄 Documento actualizado: {file_name}")
else: else:
# Crear nuevo documento # Crear nuevo documento
@@ -875,20 +860,15 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
size=len(file_content), size=len(file_content),
extension=extension extension=extension
) )
documents_created += 1 documents_created += 1
print(f"📄 Nuevo documento creado: {file_name}")
except Exception as e: except Exception as e:
print(f"❌ Error al crear documento: {str(e)}") print(f"❌ Error al procesar documento {file_name}: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') # Continuar con otros documentos
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.") print(f"🏁 Procesamiento completado. Archivos procesados en este directorio.")
except Exception as e: except Exception as e:
return Response( return Response(
{"error": f"Error durante el procesamiento: {str(e)}"}, {"error": f"Error durante el procesamiento: {str(e)}"},
@@ -903,22 +883,38 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
response_data = { response_data = {
"created_count": len(created_pedimentos), "created_count": len(created_pedimentos),
"created_pedimentos": created_pedimentos, "created_pedimentos": created_pedimentos,
"already_existing_count": len(already_existing_pedimentos),
"already_existing": already_existing_pedimentos,
"documents_created": documents_created, "documents_created": documents_created,
"failed_files": failed_files, "failed_files": failed_files,
"processed_files": len(archivos), "processed_files": len(archivos),
"summary": f"Procesados {len(archivos)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {documents_created} documento(s) asociado(s)" "summary": f"Procesados {len(archivos)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {len(already_existing_pedimentos)} ya existían, {documents_created} documento(s) asociado(s)"
} }
if failed_files: try:
# Determinar el mensaje apropiado
if already_existing_pedimentos and not created_pedimentos and not failed_files:
response_data["message"] = "Todos los pedimentos ya existen. No se crearon nuevos pedimentos."
response_status = status.HTTP_200_OK
elif already_existing_pedimentos or failed_files:
response_data.update({ response_data.update({
"message": "Procesamiento completado con algunos errores", "message": "Procesamiento completado con advertencias",
"errors": [item["error"] for item in failed_files]
}) })
if failed_files:
response_data["errors"] = [item["error"] for item in failed_files]
response_status = status.HTTP_207_MULTI_STATUS response_status = status.HTTP_207_MULTI_STATUS
else: else:
response_data["message"] = "Pedimentos creados exitosamente" response_data["message"] = "Pedimentos creados exitosamente"
response_status = status.HTTP_201_CREATED response_status = status.HTTP_201_CREATED
except Exception as e:
return Response(
{"error": f"Error durante el procesamiento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
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]) @action(detail=False, methods=['post'], url_path='bulk-create-pedimento_desk', parser_classes=[MultiPartParser, FormParser])
@@ -1781,6 +1777,66 @@ class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
my_tags = ['Importadores'] my_tags = ['Importadores']
class EjecutarComandoView(APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
"""
View para ejecutar el comando de microservicios desde una petición HTTP.
"""
def post(self, request):
# Obtener organizacion_id del request (si se envía)
organizacion_id_request = request.data.get('organizacionid', None)
procesamiento = request.data.get('procesamiento', None)
todos = request.data.get('todos', False)
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
if organizacion_id_request is None:
return Response(
{"error": 'No se proporcionó la organización a ejecutar el proceso.'},
status=status.HTTP_400_BAD_REQUEST
)
# organizacion_id = self.request.user.organizacion.id
organizacion_id = organizacion_id_request
nombre_organizacion = self.request.user.organizacion.nombre
if procesamiento is None and todos == False:
return Response(
{"message": 'No se detectó el tipo de ejecución de procesamiento.'},
status=status.HTTP_400_BAD_REQUEST
)
procesamiento = str(procesamiento)
from api.customs.tasks import microservice_v2
if todos:
microservice_v2.ejecutar_todos_por_organizacion(organizacion_id)
return Response(
{"message": f'Se estarán ejecutando todos los procesos para la organización {nombre_organizacion} en segundo plano.'},
status=status.HTTP_200_OK
)
elif organizacion_id:
if procesamiento:
microservice_v2.ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento)
return Response(
{"message": f'Se estará ejecutando el procesamiento {procesamiento} para la organización {nombre_organizacion} en segundo plano.'},
status=status.HTTP_200_OK
)
return Response(
{"error": "Parámetros insuficientes. Proporcione 'organizacion' y 'procesamiento', o seleccione 'todos'."},
status=status.HTTP_400_BAD_REQUEST
)
my_tags = ['Procesamientos_Pedimentos']
# helper | reglas para formato de docuemnto antes de cargarlo # helper | reglas para formato de docuemnto antes de cargarlo
def normalize_filename(filename): def normalize_filename(filename):
""" """

View File

@@ -667,28 +667,36 @@ 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(
@@ -712,7 +720,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': 'Registro de documentos de petición y respuesta de partidas no encontrado'}, {'error': f'Registro de documentos de petición y respuesta de {vista} no encontrado(s)'},
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND
) )

View File

@@ -61,18 +61,35 @@ 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
serializer.save() datastage = 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:
serializer.save(organizacion=self.request.user.organizacion) datastage = serializer.save(organizacion=self.request.user.organizacion)
else: else:
serializer.save() datastage = 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.
@@ -113,6 +130,7 @@ 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,6 +15,7 @@ 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)
@@ -22,6 +23,13 @@ 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') fields = ('id', 'organizacion', 'pedimento', 'pedimento_numero', 'archivo', 'document_type', 'size', 'extension', 'fuente','fuente_nombre','created_at', 'updated_at','vu')
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,6 +313,85 @@ 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):
""" """
@@ -425,9 +504,22 @@ 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 # Eliminar los documentos (archivos físicos y registros de BD)
deleted_count = existing_documents.count() archivos_eliminados = 0
existing_documents.delete() for doc in existing_documents:
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(
@@ -437,7 +529,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 = [f"No se encontró el documento con ID {id} o no pertenece a su organización" for id in 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])
# 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)
@@ -449,7 +541,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"space_freed_mb": space_freed_mb "space_freed_mb": space_freed_mb
} }
if failed_ids: if errors or 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,7 +8,8 @@ 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'] fields = ['servicio', 'pedimento_app', 'pedimento', 'timestamp_gte', 'timestamp_lte', 'status', 'organizacion']

View File

@@ -4,6 +4,7 @@ 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
@@ -22,7 +23,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): class TaskViewSet(LoggingMixin,viewsets.ModelViewSet,OrganizacionFiltradaMixin):
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
@@ -33,3 +34,18 @@ class TaskViewSet(LoggingMixin,viewsets.ModelViewSet):
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

0
api/utils/__init__.py Normal file
View File

194
api/utils/helpers.py Normal file
View File

@@ -0,0 +1,194 @@
# 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)}