Compare commits

13 Commits

Author SHA1 Message Date
Dulce
f8379807f8 agregar expedientes mediante celery 2026-01-19 09:46:35 -07:00
Dulce
3272cd1d17 actualizacion endpoint de vucem, permite a los super usuarios personalizar su organizacion 2026-01-16 09:51:40 -07:00
55a4036543 Merge pull request 'fix: se agrega funcionalidad de poder seleccionar resgistros en la vistas de partidas, coves, pedimento, edoc. Tambien se habilito la funcionalidad de poder eliminar los registros seleccionados.' (#12) from T2025-10-152-001 into main
Reviewed-on: #12
2026-01-07 22:09:30 +00:00
39c09fa445 fix: se agrega funcionalidad de poder seleccionar resgistros en la vistas de partidas, coves, pedimento, edoc. Tambien se habilito la funcionalidad de poder eliminar los registros seleccionados. 2026-01-07 14:52:49 -07:00
dfcbebb98a Merge pull request 'fix: Se crean endpoints para mostrar la informacion de peticiones y respuestas de los webservices, en el area del auditor del sistema.' (#11) from Fix--Auditor-backend into main
Reviewed-on: #11
2026-01-07 17:38:01 +00:00
b3c5c5fa87 Merge pull request 'mitigar la duplicidad de archivos a la hora de hacer bulk de documentos' (#10) from duplicidad-documentos into main
Reviewed-on: #10
2026-01-07 17:37:07 +00:00
8a4e732703 fix: Se crean endpoints para mostrar la informacion de peticiones y respuestas de los webservices, en el area del auditor del sistema. 2026-01-02 08:07:30 -07:00
Dulce
4b2f3192d0 mitigar la duplicidad de archivos a la hora de hacer bulk de documentos 2025-12-29 07:22:30 -07:00
22f1bc5390 Update api/reports/tasks/report_document.py 2025-12-16 16:01:35 +00:00
fdbc7ba4db Merge pull request 'correccion-reportes' (#9) from correccion-reportes into main
Reviewed-on: #9
2025-12-16 16:00:57 +00:00
fb843954b6 Merge pull request 'FIX2025-10-021' (#8) from FIX2025-10-021 into main
Reviewed-on: #8
2025-12-16 15:59:18 +00:00
1cb2830d71 fix: se quitan los print del endpoint bulk-create-pedimento_desk 2025-12-16 08:30:49 -07:00
942847680a Fix: Se crea nuevo endpoint para subir documentos a expediente electronico. 2025-12-16 07:29:54 -07:00
13 changed files with 3565 additions and 429 deletions

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.
@@ -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

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

View File

@@ -1,3 +1,6 @@
import os
from datetime import datetime
from django.db import models
from celery import shared_task, group from celery import shared_task, group
from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument
from core.utils import xml_controller from core.utils import xml_controller
@@ -198,7 +201,7 @@ def auditar_coves(organizacion_id):
pedimento, pedimento,
servicio=8, servicio=8,
related_name='coves', related_name='coves',
variable='acuse_descargado', variable='cove_descargado',
mensaje='COVE' mensaje='COVE'
) )
@@ -247,7 +250,7 @@ def auditar_cove_por_pedimento(pedimento_id):
pedimento, pedimento,
servicio=8, servicio=8,
related_name='coves', related_name='coves',
variable='acuse_descargado', variable='cove_descargado',
mensaje='COVE' mensaje='COVE'
) )
return {'success': True, 'pedimento_id': str(pedimento_id)} return {'success': True, 'pedimento_id': str(pedimento_id)}
@@ -302,3 +305,154 @@ def auditar_acuse_por_pedimento(pedimento_id):
except Exception as e: except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)} return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_pedimento_por_id(pedimento_id):
"""
Tarea para auditar un pedimento específico verificando todos sus documentos y datos.
"""
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
resultado = {
'pedimento_id': str(pedimento_id),
'pedimento': pedimento.pedimento,
'pedimento_app': pedimento.pedimento_app,
'organizacion': str(pedimento.organizacion.id),
'fecha_auditoria': datetime.now().isoformat(),
'estado_general': 'EN_PROGRESO',
'detalles': {}
}
# 1. Verificar documentos XML
from api.record.models import Document
documentos_xml = Document.objects.filter(
pedimento=pedimento,
archivo__endswith='.xml'
)
resultado['detalles']['documentos_xml'] = {
'total': documentos_xml.count(),
'archivos': []
}
for doc in documentos_xml:
try:
xml_info = {
'id': str(doc.id),
'nombre': os.path.basename(doc.archivo.name),
'tamanio': doc.size,
'extension': doc.extension,
'tipo': doc.document_type.descripcion if doc.document_type else 'Desconocido'
}
# Verificar si el archivo existe físicamente
if os.path.exists(doc.archivo.path):
xml_info['existe_fisicamente'] = True
# Intentar leer el XML
try:
with open(doc.archivo.path, 'r', encoding='utf-8') as f:
content = f.read()
xml_info['es_xml_valido'] = '<?xml' in content[:100]
xml_info['tamanio_bytes'] = len(content)
except Exception as e:
xml_info['error_lectura'] = str(e)
else:
xml_info['existe_fisicamente'] = False
except Exception as e:
xml_info['error'] = str(e)
resultado['detalles']['documentos_xml']['archivos'].append(xml_info)
# 2. Verificar si hay documentos asociados
resultado['detalles']['documentos_totales'] = {
'total': pedimento.documents.count(),
'por_tipo': {}
}
for doc_type in pedimento.documents.values('document_type__descripcion').annotate(total=models.Count('id')):
tipo = doc_type['document_type__descripcion'] or 'Sin tipo'
resultado['detalles']['documentos_totales']['por_tipo'][tipo] = doc_type['total']
# 3. Verificar COVEs
resultado['detalles']['coves'] = {
'total': pedimento.coves.count(),
'descargados': pedimento.coves.filter(cove_descargado=True).count(),
'con_acuse': pedimento.coves.filter(acuse_cove_descargado=True).count()
}
# 4. Verificar EDocuments
resultado['detalles']['edocuments'] = {
'total': pedimento.documentos.count(),
'descargados': pedimento.documentos.filter(edocument_descargado=True).count(),
'con_acuse': pedimento.documentos.filter(acuse_descargado=True).count()
}
# 5. Verificar procesamientos
resultado['detalles']['procesamientos'] = {
'total': pedimento.procesamientos.count(),
'por_estado': {}
}
for proc in pedimento.procesamientos.values('estado__estado').annotate(total=models.Count('id')):
estado = proc['estado__estado'] or 'Sin estado'
resultado['detalles']['procesamientos']['por_estado'][estado] = proc['total']
# 6. Verificar campos importantes del pedimento
campos_revisados = {
'numero_operacion': bool(pedimento.numero_operacion),
'numero_partidas': bool(pedimento.numero_partidas),
'importe_total': bool(pedimento.importe_total),
'contribuyente': bool(pedimento.contribuyente),
'tiene_remesas': pedimento.remesas,
'partidas_creadas': pedimento.partidas.count() > 0,
'fecha_pago': bool(pedimento.fecha_pago)
}
resultado['detalles']['campos_pedimento'] = campos_revisados
resultado['detalles']['campos_completos'] = sum(campos_revisados.values())
resultado['detalles']['campos_totales'] = len(campos_revisados)
# 7. Determinar estado general
campos_completos = resultado['detalles']['campos_completos']
total_campos = resultado['detalles']['campos_totales']
if documentos_xml.count() == 0:
resultado['estado_general'] = 'SIN_XML'
resultado['mensaje'] = 'No se encontraron documentos XML'
elif campos_completos == total_campos:
resultado['estado_general'] = 'COMPLETO'
resultado['mensaje'] = 'Pedimento completamente procesado'
elif campos_completos >= total_campos * 0.7:
resultado['estado_general'] = 'PARCIAL'
resultado['mensaje'] = 'Pedimento parcialmente procesado'
else:
resultado['estado_general'] = 'INCOMPLETO'
resultado['mensaje'] = 'Pedimento con información incompleta'
resultado['porcentaje_completitud'] = (campos_completos / total_campos) * 100 if total_campos > 0 else 0
# 8. Sugerencias
sugerencias = []
if not pedimento.numero_operacion:
sugerencias.append("Falta el número de operación")
if not pedimento.numero_partidas:
sugerencias.append("Falta el número de partidas")
if pedimento.numero_partidas and pedimento.numero_partidas > pedimento.partidas.count():
sugerencias.append(f"Faltan partidas: {pedimento.numero_partidas - pedimento.partidas.count()} de {pedimento.numero_partidas}")
if not pedimento.contribuyente:
sugerencias.append("Falta el contribuyente asociado")
resultado['sugerencias'] = sugerencias
return resultado
except Pedimento.DoesNotExist:
return {
'error': f'Pedimento con ID {pedimento_id} no encontrado',
'pedimento_id': str(pedimento_id)
}
except Exception as e:
return {
'error': f'Error auditar pedimento {pedimento_id}: {str(e)}',
'pedimento_id': str(pedimento_id)
}

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

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

@@ -42,7 +42,24 @@ from .views_auditor import (
auditar_acuse_cove_pedimento_endpoint, auditar_acuse_cove_pedimento_endpoint,
auditar_edocument_pedimento_endpoint, auditar_edocument_pedimento_endpoint,
auditar_acuse_pedimento_endpoint, auditar_acuse_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion auditar_procesamiento_remesa_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion,
auditar_peticion_respuesta_pedimento_completo,
auditor_obtener_peticion_pedimento_vu,
auditor_obtener_respuesta_pedimento_vu,
auditor_obtener_peticion_remesa_vu,
auditor_obtener_respuesta_remesa_vu,
auditor_obtener_peticion_partidas_vu,
auditor_obtener_respuesta_partidas_vu,
auditor_obtener_peticion_acuse_vu,
auditor_obtener_respuesta_acuse_vu,
auditor_obtener_peticion_cove_vu,
auditor_obtener_respuesta_cove_vu,
auditor_obtener_peticion_acuse_cove_vu,
auditor_obtener_respuesta_acuse_cove_vu,
auditor_obtener_peticion_edocument_vu,
auditor_obtener_respuesta_edocument_vu,
auditar_pedimento_endpoint,
) )
urlpatterns = [ urlpatterns = [
@@ -58,5 +75,24 @@ urlpatterns = [
path('auditor/auditar-acuse-cove/pedimento/', auditar_acuse_cove_pedimento_endpoint, name='auditar-acuse-cove-pedimento'), path('auditor/auditar-acuse-cove/pedimento/', auditar_acuse_cove_pedimento_endpoint, name='auditar-acuse-cove-pedimento'),
path('auditor/auditar-edocument/pedimento/', auditar_edocument_pedimento_endpoint, name='auditar-edocument-pedimento'), path('auditor/auditar-edocument/pedimento/', auditar_edocument_pedimento_endpoint, name='auditar-edocument-pedimento'),
path('auditor/auditar-acuse/pedimento/', auditar_acuse_pedimento_endpoint, name='auditar-acuse-pedimento'), path('auditor/auditar-acuse/pedimento/', auditar_acuse_pedimento_endpoint, name='auditar-acuse-pedimento'),
path('auditor/auditar-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-remesa-pedimento'),
path('auditor/auditar-pedimento/', auditar_pedimento_endpoint, name='auditar-pedimento'),
path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'), path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'),
path('auditor/peticion-respuesta/pedimento-vu/', auditar_peticion_respuesta_pedimento_completo, name='peticion-respuesta-pedimento-vu'),
path('auditor/obtener-peticion/pedimento-vu/', auditor_obtener_peticion_pedimento_vu, name='obtener-peticion-pedimento-vu'),
path('auditor/obtener-respuesta/pedimento-vu/', auditor_obtener_respuesta_pedimento_vu, name='obtener-respuesta-pedimento-vu'),
path('auditor/obtener-peticion/remesa-vu/', auditor_obtener_peticion_remesa_vu, name='obtener-peticion-remesa-vu'),
path('auditor/obtener-respuesta/remesa-vu/', auditor_obtener_respuesta_remesa_vu, name='obtener-respuesta-remesa-vu'),
path('auditor/obtener-peticion/partidas-vu/', auditor_obtener_peticion_partidas_vu, name='obtener-peticion-partidas-vu'),
path('auditor/obtener-respuesta/partidas-vu/', auditor_obtener_respuesta_partidas_vu, name='obtener-respuesta-partidas-vu'),
path('auditor/obtener-peticion/acuse-vu/', auditor_obtener_peticion_acuse_vu, name='obtener-peticion-acuse-vu'),
path('auditor/obtener-respuesta/acuse-vu/', auditor_obtener_respuesta_acuse_vu, name='obtener-respuesta-acuse-vu'),
path('auditor/obtener-peticion/cove-vu/', auditor_obtener_peticion_cove_vu, name='obtener-peticion-cove-vu'),
path('auditor/obtener-respuesta/cove-vu/', auditor_obtener_respuesta_cove_vu, name='obtener-respuesta-cove-vu'),
path('auditor/obtener-peticion/acuse-cove-vu/', auditor_obtener_peticion_acuse_cove_vu, name='obtener-peticion-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-respuesta/edocument-vu/', auditor_obtener_respuesta_edocument_vu, name='obtener-respuesta-edocument-vu'),
] ]

View File

@@ -9,6 +9,7 @@ from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework import status from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.core.files.storage import default_storage
from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.filters import SearchFilter, OrderingFilter
from core.permissions import ( from core.permissions import (
IsSameOrganization, IsSameOrganization,
@@ -23,7 +24,7 @@ from api.customs.models import (
EDocument, EDocument,
Cove, Cove,
Importador, Importador,
Partida Partida,
) )
from api.customs.serializers import ( from api.customs.serializers import (
PedimentoSerializer, PedimentoSerializer,
@@ -44,12 +45,14 @@ import zipfile
import tempfile import tempfile
import shutil import shutil
import subprocess import subprocess
from datetime import datetime from datetime import date, datetime, time
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.parsers import MultiPartParser, FormParser
from api.record.models import Document, DocumentType from api.record.models import Document, DocumentType, Fuente
from unicodedata import normalize
from django.utils import timezone
from .tasks.bulk_pedimentos import process_bulk_upload
# Importar rarfile de manera opcional # Importar rarfile de manera opcional
try: try:
import rarfile import rarfile
@@ -57,6 +60,9 @@ try:
except ImportError: except ImportError:
RAR_SUPPORT = False RAR_SUPPORT = False
# Importar tarea de procesamiento de pedimento (Celery)
from api.customs.tasks.microservice import procesar_pedimento_completo_individual
def get_available_extractors(): def get_available_extractors():
""" """
Devuelve lista de extractores disponibles en orden de preferencia Devuelve lista de extractores disponibles en orden de preferencia
@@ -281,7 +287,16 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
ordering = ['-created_at'] # Orden descendente por fecha de creación por defecto ordering = ['-created_at'] # Orden descendente por fecha de creación por defecto
def get_queryset(self): def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
# pedimento_app_filter = self.request.GET.get('pedimento_app', None)
# if pedimento_app_filter:
# print(f"Filtro por pedimento_app: {pedimento_app_filter}")
# queryset = queryset.filter(pedimento_app__icontains=pedimento_app_filter)
return queryset
def perform_create(self, serializer): def perform_create(self, serializer):
""" """
@@ -360,6 +375,25 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
] ]
} }
@action(detail=True, methods=['post'], url_path='procesar-completo')
def procesar_completo(self, request, pk=None):
"""
Acción para disparar el procesamiento completo de un pedimento existente.
Dispara la tarea `procesar_pedimento_completo_individual` de forma asíncrona
y devuelve el `task_id`.
"""
pedimento = self.get_object()
try:
# Usar el nombre del servicio de Docker Compose en lugar de localhost
task = procesar_pedimento_completo_individual.delay(pedimento.id, pedimento.organizacion.id)
# Verificar si la respuesta fue exitosa
if task.id:
return Response({"status": "Recurso creado exitosamente en API", "task_id": task.id}, status=status.HTTP_202_ACCEPTED)
else:
return Response({"status": "Servicio API respondió con error", "task_id": 0}, status=status.HTTP_202_ACCEPTED)
except Exception as e:
return Response({"error": f"Error inesperado al llamar al servicio API: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@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):
""" """
@@ -455,434 +489,313 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
@action(detail=False, methods=['post'], url_path='bulk-create', parser_classes=[MultiPartParser, FormParser]) @action(detail=False, methods=['post'], url_path='bulk-create', parser_classes=[MultiPartParser, FormParser])
def bulk_create(self, request): def bulk_create(self, request):
""" """
Endpoint para crear múltiples pedimentos de manera masiva desde archivos. Version asincrona con Celery
FormData esperado:
- contribuyente: string (nombre del contribuyente)
- archivos: files (pueden ser múltiples archivos: zip, rar o individuales)
Nomenclatura esperada de archivos: anio-aduana-patente-pedimento
- anio: 2 dígitos (ej: 24)
- aduana: 2 o 3 dígitos (ej: 01, 123)
- patente: 4 dígitos (ej: 3420)
- pedimento: 7 dígitos (ej: 1234567)
Ejemplo: 24-01-3420-1234567
Nota: Cada archivo ZIP/RAR se procesa independientemente en su propio subdirectorio.
Respuesta exitosa:
{
"message": "Pedimentos creados exitosamente",
"created_count": 5,
"created_pedimentos": [...],
"documents_created": 15,
"processed_files": 3,
"summary": "Procesados 3 archivo(s): 5 pedimento(s) creado(s), 15 documento(s) asociado(s)",
"failed_files": [],
"errors": []
}
""" """
print(request.data)
# Validar datos requeridos from django.utils import timezone
import time as python_time
contribuyente = request.data.get('contribuyente') contribuyente = request.data.get('contribuyente')
archivos = request.FILES.getlist('archivos') archivos = request.FILES.getlist('archivos')
if not contribuyente:
return Response(
{"error": "Se requiere el campo 'contribuyente'"},
status=status.HTTP_400_BAD_REQUEST
)
if not archivos: if not archivos:
return Response( return Response(
{"error": "Se requiere al menos un archivo"}, {"error": "Se requiere al menos un archivo"},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
# Validar organización del usuario
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
return Response( return Response(
{"error": "Usuario no autenticado o sin organización"}, {"error": "Usuario no autenticado o sin organización"},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
organizacion = request.user.organizacion temp_files_info = []
# Regex para validar nomenclatura: anio-aduana-patente-pedimento
nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$')
nomenclatura_pattern_sin_anio = re.compile(r'^(\d{2,3})-(\d{4})-(\d{7})$')
created_pedimentos = []
failed_files = []
errors = []
documents_created = 0
temp_dir = None
# Obtener DocumentType ANTES de la transacción atómica
print("Intentando obtener o crear DocumentType...")
try:
# Primero intentar obtener si ya existe
try:
document_type = DocumentType.objects.get(nombre="Pedimento")
print(f"DocumentType obtenido existente: {document_type.nombre} (ID: {document_type.id})")
except DocumentType.DoesNotExist:
# Si no existe, crear uno nuevo
document_type = DocumentType.objects.create(
nombre="Pedimento",
descripcion="Documento de pedimento"
)
print(f"DocumentType creado nuevo: {document_type.nombre} (ID: {document_type.id})")
except Exception as e:
print(f"Error al obtener/crear DocumentType: {str(e)}")
# Como fallback, intentar obtener cualquier DocumentType existente
try:
document_type = DocumentType.objects.first()
if document_type:
print(f"Usando DocumentType existente como fallback: {document_type.nombre} (ID: {document_type.id})")
else:
print("No hay DocumentType disponible")
return Response(
{"error": "No se pudo configurar el tipo de documento y no hay tipos existentes"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except Exception as fallback_error:
print(f"Error en fallback: {str(fallback_error)}")
return Response(
{"error": f"Error crítico al configurar tipo de documento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
try: try:
print("Iniciando transacción atómica...") for archivo in archivos:
with transaction.atomic(): timestamp = int(timezone.now().timestamp() * 1000)
# Crear directorio temporal unique_name = f"temp_{timestamp}_{archivo.name}"
temp_dir = tempfile.mkdtemp() saved_path = f"temp_uploads/{unique_name}"
print(f"Directorio temporal creado: {temp_dir}") default_storage.save(saved_path, ContentFile(archivo.read()))
# Procesar cada archivo enviado temp_files_info.append({
for idx, archivo in enumerate(archivos): 'original_name': archivo.name,
archivo_name = archivo.name.lower() 'saved_path': saved_path,
print(f"Procesando archivo {idx + 1}/{len(archivos)}: {archivo_name}") 'size': archivo.size
# Crear subdirectorio para cada archivo usando el nombre del archivo sin extensión
archivo_name_sin_extension = os.path.splitext(archivo.name)[0]
sub_dir = os.path.join(temp_dir, archivo_name_sin_extension)
os.makedirs(sub_dir, exist_ok=True)
print(f"Subdirectorio creado: {sub_dir}")
if archivo_name.endswith('.zip'):
# Manejar archivo ZIP
print("Es un archivo ZIP")
try:
with zipfile.ZipFile(archivo, 'r') as zip_ref:
zip_ref.extractall(sub_dir)
print("Archivo ZIP extraído exitosamente")
except zipfile.BadZipFile as e:
return Response(
{"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:
print(f"Nomenclatura inválida: {folder_name}")
# Determinar el archivo original basado en el subdirectorio
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Nomenclatura inválida: {folder_name}. Esperado: anio-aduana-patente-pedimento"
}) })
continue
if match: bulk_upload = BulkUploadTask.objects.create(
user=request.user,
print(f"Nomenclatura válida: {folder_name}") organizacion=request.user.organizacion,
anio, aduana, patente, pedimento_num = match.groups() contribuyente=contribuyente,
print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") task_type='bulk_create',
# Formato original: anio-aduana-patente-pedimento total_files=len(archivos),
# Crear fecha_pago basada en el año status='pending',
try: result={
# Convertir año de 2 dígitos a 4 dígitos 'temp_files': temp_files_info,
anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio) 'original_filenames': [archivo.name for archivo in archivos]
fecha_pago = datetime(anio_completo, 1, 1).date()
print(f"Fecha de pago calculada: {fecha_pago}")
except ValueError:
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Año inválido: {anio}"
})
continue
elif match_sin_anio:
print(f"Nomenclatura válida sin año: {folder_name}")
# Formato sin año: aduana-patente-pedimento
aduana, patente, pedimento_num = match_sin_anio.groups()
print(f"Extraído - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
# Obtener el primer dígito del pedimento
primer_digito_pedimento = int(pedimento_num[0]) if pedimento_num else 0
# Usar año actual para fecha_pago y ajustar según el dígito del pedimento
año_actual = datetime.now().year
# Crear año con el dígito del pedimento (reemplazando el último dígito)
año_con_digito = int(str(año_actual)[:-1] + str(primer_digito_pedimento))
# Aplicar lógica de comparación
if año_con_digito <= año_actual:
# Si el año con dígito es menor o igual al año actual
año_final = año_con_digito
else:
# Si el año con dígito es mayor al año actual, restar 10
año_final = año_con_digito - 10
# Tomar los últimos 2 dígitos del año final
anio = año_final % 100
# Crear fecha de pago (primer día del año)
fecha_pago = datetime(año_final , 1, 1).date()
print(f"Fecha de pago (año actual) calculada: {fecha_pago}")
# Generar pedimento_app
pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}"
print(f"Pedimento_app generado: {pedimento_app}")
print(f"Buscando pedimento existente con pedimento_app: {pedimento_app} y organización ID: {organizacion.id}")
# Verificar si el pedimento ya existe
existing_pedimento = Pedimento.objects.filter(
pedimento_app=pedimento_app,
# organizacion=organizacion
).first()
print(f"Pedimento existente: {existing_pedimento is not None}")
if not existing_pedimento:
print("📝 Pedimento no existe, creando nuevo...")
# Crear nuevo pedimento
try:
print("🔄 Iniciando creación de pedimento...")
# Obtener o crear el importador
print(f"🏢 Buscando/creando importador con RFC: {contribuyente}")
importador, created = Importador.objects.get_or_create(
rfc=contribuyente,
defaults={
'nombre': f"Importador {contribuyente}",
'organizacion': organizacion
} }
) )
if created:
print(f"✅ Importador creado: {importador.rfc} - {importador.nombre}")
else:
print(f"♻️ Importador existente: {importador.rfc} - {importador.nombre}")
pedimento = Pedimento.objects.create( process_bulk_upload.delay(
organizacion=organizacion, bulk_upload_id=bulk_upload.id
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}", # Valor por defecto
clave_pedimento="A1" # Valor por defecto
) )
print(f"✅ Pedimento creado exitosamente: ID {pedimento.id}, pedimento_app: {pedimento_app}") return Response({
"message": "Procesamiento iniciado en segundo plano",
created_pedimentos.append({ "task_id": bulk_upload.id,
"id": str(pedimento.id), "status": "pending",
"pedimento_app": pedimento_app, "check_status_url": f"/api/pedimentos/bulk-upload-status/{bulk_upload.id}/"
"contribuyente": importador.rfc, }, status=status.HTTP_202_ACCEPTED)
"contribuyente_nombre": importador.nombre
})
except Exception as e: except Exception as e:
print(f"❌ Error al crear pedimento: {str(e)}") for file_info in temp_files_info:
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Error al crear pedimento: {str(e)}"
})
continue
else:
print(f"♻️ Usando pedimento existente: ID {existing_pedimento.id}")
# Usar pedimento existente
pedimento = existing_pedimento
print(f"🔄 Iniciando creación de documento para pedimento ID: {pedimento.id}")
# Crear documento asociado al pedimento
try: try:
print("📖 Leyendo archivo desde directorio temporal...") default_storage.delete(file_info['saved_path'])
# Leer el archivo desde el directorio temporal except:
with open(file_path, 'rb') as f: pass
file_content = f.read()
print(f"📄 Archivo leído: {len(file_content)} bytes")
# Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name)
# # Verificar si el documento ya existe para este pedimento y archivo
# print("🔍 Verificando existencia previa del documento...")
# # Reemplazar múltiples caracteres
# normalized_file_name = file_name.replace(" ", "_")
# file_name_without_extension = normalized_file_name.rsplit('.', 1)[0]
# extension_file = os.path.splitext(normalized_file_name)[1].lower().lstrip('.')
# existing_document = Document.objects.filter(
# pedimento_id=pedimento.id,
# archivo__contains=file_name_without_extension,
# extension=extension_file
# ).first()
# if existing_document:
# print(f"Documento existente encontrado, omitiendo creación: ID {existing_document.id}")
# continue
print(f"Creando documento para archivo: {file_name}")
# Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento.id,
document_type=document_type,
fuente_id=4, # Fuente: Carga Plataforma
archivo=django_file,
size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.')
)
print(f"Documento creado exitosamente: {document.id}")
documents_created += 1
print(f"📊 Total documentos creados hasta ahora: {documents_created}")
except Exception as e:
print(f"❌ Error al crear documento: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Error al crear documento: {str(e)}"
})
continue
print(f"🏁 Procesamiento completado. Archivos procesados en este directorio.")
except Exception as e:
return Response( return Response(
{"error": f"Error durante el procesamiento: {str(e)}"}, {"error": f"Error al iniciar procesamiento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
finally:
# Limpiar directorio temporal
if temp_dir and os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
# Preparar respuesta @action(detail=False, methods=['post'], url_path='bulk-create-pedimento_desk', parser_classes=[MultiPartParser, FormParser])
def bulk_create_pedimento_desk(self, request):
"""
Versión asíncrona para EFC APP Desk
"""
print("🔧 Iniciando bulk-create-pedimento_desk asíncrono...")
from django.utils import timezone
archivos = request.FILES.getlist('archivos')
if not archivos:
return Response(
{"tieneError": True, "error": "Se requiere al menos un archivo"},
status=status.HTTP_400_BAD_REQUEST
)
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
return Response(
{"tieneError": True, "error": "Usuario no autenticado o sin organización"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
temp_files_info = []
try:
for archivo in archivos:
timestamp = int(timezone.now().timestamp() * 1000)
unique_name = f"temp_{timestamp}_{archivo.name}"
saved_path = f"temp_uploads/{unique_name}"
default_storage.save(saved_path, ContentFile(archivo.read()))
temp_files_info.append({
'original_name': archivo.name,
'saved_path': saved_path,
'size': archivo.size
})
bulk_upload = BulkUploadTask.objects.create(
user=request.user,
organizacion=request.user.organizacion,
contribuyente=request.data.get('contribuyente'),
task_type='bulk_create_pedimento_desk',
total_files=len(archivos),
status='pending',
fecha_pago=request.data.get('fecha_pago'),
clave_pedimento=request.data.get('clave_pedimento'),
tipo_operacion_id=request.data.get('tipo_operacion'),
curp_apoderado=request.data.get('curp_apoderado'),
partidas=request.data.get('partidas', 0),
result={
'temp_files': temp_files_info,
'original_filenames': [archivo.name for archivo in archivos]
}
)
process_bulk_upload.delay(
bulk_upload_id=bulk_upload.id
)
return Response({
"tieneError": False,
"message": "Procesamiento iniciado en segundo plano",
"task_id": bulk_upload.id,
"status": "pending",
"check_status_url": f"/api/pedimentos/bulk-upload-status/{bulk_upload.id}/"
}, status=status.HTTP_202_ACCEPTED)
except Exception as e:
for file_info in temp_files_info:
try:
default_storage.delete(file_info['saved_path'])
except:
pass
return Response(
{"tieneError": True, "error": f"Error al iniciar procesamiento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=False, methods=['get'], url_path='bulk-upload-status(?:/(?P<task_id>[^/.]+))?')
def bulk_upload_status(self, request, task_id=None):
"""
Endpoint que puede manejar:
1. Un task_id específico: /bulk-upload-status/<task_id>/
2. Todos los tasks pendientes del usuario: /bulk-upload-status/
Respuestas:
- Con task_id: Información detallada de una tarea específica
- Sin task_id: Lista de todas las tareas del usuario con status != 'completed'
"""
try:
if task_id:
bulk_upload = BulkUploadTask.objects.get(id=task_id, user=request.user)
estimated_documents = bulk_upload.created_documents
estimated_percentage = 0
if bulk_upload.status == 'processing' and bulk_upload.started_at:
elapsed = (timezone.now() - bulk_upload.started_at).total_seconds()
if elapsed > 10 and bulk_upload.created_documents > 0:
docs_per_second = bulk_upload.created_documents / elapsed
estimated_total_seconds = 300 # 5 minutos para 1000 docs
estimated_documents = int(docs_per_second * min(elapsed, estimated_total_seconds))
estimated_percentage = min(95, (elapsed / estimated_total_seconds) * 100)
elif bulk_upload.status == 'completed' and bulk_upload.result:
estimated_documents = bulk_upload.created_documents
estimated_percentage = 100
response_data = { response_data = {
"created_count": len(created_pedimentos), "task_id": bulk_upload.id,
"created_pedimentos": created_pedimentos, "status": bulk_upload.status,
"documents_created": documents_created, "celery_status": {
"failed_files": failed_files, 'state': 'PROCESSING' if bulk_upload.status == 'processing' else bulk_upload.status.upper(),
"processed_files": len(archivos), 'info': bulk_upload.result if bulk_upload.status == 'completed' else None
"summary": f"Procesados {len(archivos)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {documents_created} documento(s) asociado(s)" },
"progress": {
"total_files": bulk_upload.total_files,
"processed_files": bulk_upload.processed_files,
"created_pedimentos": bulk_upload.created_pedimentos,
"created_documents": bulk_upload.created_documents,
"estimated_documents": estimated_documents,
"percentage_files": (bulk_upload.processed_files / bulk_upload.total_files * 100) if bulk_upload.total_files > 0 else 0,
"percentage_documents_estimated": estimated_percentage,
"batch_info": {
"batch_size": 50,
"current_batch": (bulk_upload.created_documents // 50) + 1,
"next_update_at": bulk_upload.created_documents + (50 - (bulk_upload.created_documents % 50))
}
},
"metadata": {
"task_type": bulk_upload.task_type,
"contribuyente": bulk_upload.contribuyente,
"created_at": bulk_upload.created_at,
"started_at": bulk_upload.started_at,
"finished_at": bulk_upload.finished_at,
"elapsed_seconds": (timezone.now() - bulk_upload.started_at).total_seconds() if bulk_upload.started_at else 0
}
} }
if failed_files: if bulk_upload.status in ['completed', 'partial', 'failed']:
response_data.update({ response_data.update({
"message": "Procesamiento completado con algunos errores", "result": bulk_upload.result,
"errors": [item["error"] for item in failed_files] "failed_files": bulk_upload.failed_files,
"error_message": bulk_upload.error_message
}) })
response_status = status.HTTP_207_MULTI_STATUS
if bulk_upload.status == 'processing':
response_data["note"] = "El progreso se actualiza cada 50 documentos para optimizar rendimiento"
response_data["next_update_in"] = 50 - (bulk_upload.created_documents % 50)
return Response(response_data)
else: else:
response_data["message"] = "Pedimentos creados exitosamente" pending_tasks = BulkUploadTask.objects.filter(
response_status = status.HTTP_201_CREATED user=request.user
).exclude(
status='completed'
).order_by('-created_at')
return Response(response_data, status=response_status) if not pending_tasks.exists():
return Response({
"pending_tasks": [],
"total_pending": 0,
"message": "No hay tareas de carga pendientes"
})
tasks_list = []
for task in pending_tasks:
estimated_documents = task.created_documents
estimated_percentage = 0
if task.status == 'processing' and task.started_at:
elapsed = (timezone.now() - task.started_at).total_seconds()
if elapsed > 10 and task.created_documents > 0:
docs_per_second = task.created_documents / elapsed
estimated_total_seconds = 300
estimated_documents = int(docs_per_second * min(elapsed, estimated_total_seconds))
estimated_percentage = min(95, (elapsed / estimated_total_seconds) * 100)
elif task.status == 'completed':
estimated_percentage = 100
task_data = {
"task_id": task.id,
"status": task.status,
"task_type": task.task_type,
"contribuyente": task.contribuyente,
"total_files": task.total_files,
"created_at": task.created_at,
"started_at": task.started_at,
"finished_at": task.finished_at,
"progress": {
"processed_files": task.processed_files,
"created_pedimentos": task.created_pedimentos,
"created_documents": task.created_documents,
"estimated_documents": estimated_documents,
"percentage_files": (task.processed_files / task.total_files * 100) if task.total_files > 0 else 0,
"percentage_documents_estimated": estimated_percentage,
},
"metadata": {
"elapsed_seconds": (timezone.now() - task.started_at).total_seconds() if task.started_at else 0,
"batch_size": 50,
"next_batch_update": task.created_documents + (50 - (task.created_documents % 50))
}
}
if task.error_message:
task_data["error_message"] = task.error_message
tasks_list.append(task_data)
return Response({
"pending_tasks": tasks_list,
"total_pending": len(tasks_list),
"summary": {
"processing": len([t for t in tasks_list if t["status"] == "processing"]),
"pending": len([t for t in tasks_list if t["status"] == "pending"]),
"partial": len([t for t in tasks_list if t["status"] == "partial"]),
"failed": len([t for t in tasks_list if t["status"] == "failed"]),
}
})
except BulkUploadTask.DoesNotExist:
return Response(
{"error": "Task no encontrada"},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
return Response(
{"error": f"Error interno: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
my_tags = ['Pedimentos'] my_tags = ['Pedimentos']
@@ -1200,3 +1113,84 @@ class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador") raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador")
my_tags = ['Importadores'] my_tags = ['Importadores']
# helper | reglas para formato de docuemnto antes de cargarlo
def normalize_filename(filename):
"""
Normaliza el nombre del archivo removiendo caracteres especiales,
espacios y asegurando consistencia.
"""
filename = normalize('NFKD', filename).encode('ASCII', 'ignore').decode('ASCII')
filename = re.sub(r'[^\w\s.-]', '_', filename) # Remover caracteres no alfanuméricos
filename = re.sub(r'[\s()]+', '_', filename) # Reemplazar espacios y paréntesis
filename = re.sub(r'_+', '_', filename) # Consolidar múltiples _
filename = filename.strip('_') # Remover _ al inicio/final
return filename
def get_clean_base_filename(filename):
"""
Obtiene el nombre base limpio sin el sufijo de Django.
"""
normalized = normalize_filename(filename)
name_without_ext, ext = os.path.splitext(normalized)
django_suffix = extract_django_suffix(name_without_ext)
if django_suffix:
base_name = name_without_ext[:-8]
else:
base_name = name_without_ext
base_name = re.sub(r'(_copy|_copia|_-_copia|_-_copy)(_\d+)?$', '', base_name)
return base_name.lower().strip('_')
def is_same_document(existing_doc, new_filename):
"""
Compara si un documento existente y un nuevo archivo son el mismo documento.
Args:
existing_doc: Objeto Document existente
new_filename: Nombre del nuevo archivo a subir
Returns:
bool: True si son el mismo documento
"""
existing_basename = os.path.basename(existing_doc.archivo.name)
existing_base = get_clean_base_filename(existing_basename)
new_base = get_clean_base_filename(new_filename)
existing_ext = existing_doc.extension.lower()
new_ext = os.path.splitext(new_filename)[1].lower().lstrip('.')
return existing_base == new_base and existing_ext == new_ext
def extract_django_suffix(filename):
"""
Extrae el sufijo único que Django añade a los archivos.
"""
name_without_ext = os.path.splitext(filename)[0]
match = re.search(r'_([a-zA-Z0-9]{7})$', name_without_ext)
if match:
return match.group(1)
return None
def get_clean_base_filename(filename):
"""
Obtiene el nombre base limpio sin el sufijo de Django.
"""
normalized = normalize_filename(filename)
name_without_ext, ext = os.path.splitext(normalized)
django_suffix = extract_django_suffix(name_without_ext)
if django_suffix:
base_name = name_without_ext[:-8]
else:
base_name = name_without_ext
base_name = re.sub(r'(_copy|_copia|_-_copia|_-_copy)(_\d+)?$', '', base_name)
return base_name.lower().strip('_')

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,11 @@ from api.customs.models import Pedimento
class DocumentSerializer(serializers.ModelSerializer): class DocumentSerializer(serializers.ModelSerializer):
pedimento_numero = serializers.SerializerMethodField(read_only=True) pedimento_numero = serializers.SerializerMethodField(read_only=True)
pedimento = serializers.PrimaryKeyRelatedField(queryset=Pedimento.objects.all()) pedimento = serializers.PrimaryKeyRelatedField(queryset=Pedimento.objects.all())
fuente_nombre = serializers.SerializerMethodField()
fuente = serializers.PrimaryKeyRelatedField(queryset=Fuente.objects.all())
class Meta: class Meta:
model = Document model = Document
fields = ('id', 'organizacion', 'pedimento', 'pedimento_numero', 'archivo', 'document_type', 'size', 'extension', 'fuente','created_at', 'updated_at') fields = ('id', 'organizacion', 'pedimento', 'pedimento_numero', 'archivo', 'document_type', 'size', 'extension', 'fuente','fuente_nombre','created_at', 'updated_at')
read_only_fields = ('id', 'size', 'extension', 'created_at', 'updated_at', 'pedimento_numero') read_only_fields = ('id', 'size', 'extension', 'created_at', 'updated_at', 'pedimento_numero')
def get_pedimento_numero(self, obj): def get_pedimento_numero(self, obj):
@@ -26,6 +27,12 @@ class DocumentSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("Se requiere un archivo para subir") raise serializers.ValidationError("Se requiere un archivo para subir")
return value return value
def get_fuente_nombre(self, obj):
# Método 1: Si la fuente está precargada con select_related
if obj.fuente:
return obj.fuente.nombre
return "Desconocido"
class FuenteSerializer(serializers.ModelSerializer): class FuenteSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Fuente model = Fuente

View File

@@ -11,7 +11,8 @@ from .views import (DocumentViewSet
, DocumentTypeView , DocumentTypeView
, ExpedienteZipDownloadView , ExpedienteZipDownloadView
, MultiPedimentoZipDownloadView , MultiPedimentoZipDownloadView
, PedimentoDocumentViewSet) , PedimentoDocumentViewSet
, TriggerPedimentoCompletoView)
# Create a router and register your viewsets with it # Create a router and register your viewsets with it
@@ -35,5 +36,6 @@ urlpatterns = [
path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'), path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'),
path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'), path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'),
path('pedimento-documents/', PedimentoDocumentViewSet.as_view({'get': 'list'}), name='pedimento-document-list'), path('pedimento-documents/', PedimentoDocumentViewSet.as_view({'get': 'list'}), name='pedimento-document-list'),
path('microservice/pedimento-completo/', TriggerPedimentoCompletoView.as_view(), name='trigger-pedimento-completo'),
path('', include(router.urls)), path('', include(router.urls)),
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -244,7 +244,7 @@ def generate_report_control_pedimento(report_id):
# SECCIÓN DE TOTALES # SECCIÓN DE TOTALES
writer.writerow(['RESUMEN DEL REPORTE - CONTROL DE PEDIMENTOS']) writer.writerow(['RESUMEN DEL REPORTE - CONTROL DE PEDIMENTOS'])
writer.writerow(['OOGANIZACION:', nombre_organizacion]) writer.writerow(['ORGANIZACION:', nombre_organizacion])
writer.writerow([]) writer.writerow([])
writer.writerow(['TOTAL DE EXPEDIENTES:', pedimentos_total]) writer.writerow(['TOTAL DE EXPEDIENTES:', pedimentos_total])
writer.writerow(['TOTAL DE EXPEDIENTES COMPLETOS:', pedimentos_completos]) writer.writerow(['TOTAL DE EXPEDIENTES COMPLETOS:', pedimentos_completos])

View File

@@ -1,4 +1,5 @@
from django.shortcuts import render from django.shortcuts import render
from ..organization.models import Organizacion
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
@@ -93,7 +94,23 @@ class VucemView(viewsets.ModelViewSet):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.") raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
if self.request.user.is_superuser: if self.request.user.is_superuser:
serializer.save(created_by=self.request.user, updated_by=self.request.user) organizacion_id = self.request.data.get('organizacion_id')
if not organizacion_id:
raise ValueError("Los superusuarios deben especificar una organización")
try:
# Importa el modelo Organizacion
# from ..organization.models import Organizacion
organizacion = Organizacion.objects.get(id=organizacion_id)
except Organizacion.DoesNotExist:
raise ValueError({"organizacion": "Organización no encontrada"})
serializer.save(
organizacion=organizacion,
created_by=self.request.user,
updated_by=self.request.user
)
return return
else: else:
serializer.save( serializer.save(