Compare commits

24 Commits

Author SHA1 Message Date
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
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
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
97ac547a4b fix: Se modifica Dockerfile.prod para que instale unrar desde pagina oficial. 2025-12-10 11:44:13 -07:00
ed63a4854c fix: Se ajusta carga de expediente en RAR y se agega validacion para evitar duplicar el pedimento independientemente de la organizacion de carga. 2025-12-10 11:30:58 -07:00
202b053698 fix/reubicacion del documentos del detalle de pedimentos 2025-12-05 08:16:16 -07:00
77f9fe4389 Se habilita funcionalidad para crear pediementos con sus documentos apartir de una carpeta, zip o rar 2025-11-24 08:51:42 -07:00
72c0d70a71 Se habilita opcion de descarga de pedimentos individuales, masivos, por filtro. 2025-11-24 08:25:59 -07:00
4b44c098c4 auditor 2025-10-22 19:35:39 -06:00
aaa1e79473 Documentacion 2025-10-22 19:17:11 -06:00
91ab38fc91 Documentacion 2025-10-22 19:08:08 -06:00
0e0572125a microservicios 2025-10-22 18:34:40 -06:00
73413fe3d9 microservicios 2025-10-22 18:32:46 -06:00
3b19520481 repo 2025-10-21 21:44:51 -06:00
474cb151ef Reportes 2025-10-21 18:47:15 -06:00
14c06cbf43 reportes 2025-10-20 21:14:31 -06:00
265f471ea6 Merge pull request 'Se crea endpoint para procesar todos los pedimentos' (#3) from feature/Agendar-todos into main
Reviewed-on: #3
2025-10-21 01:28:25 +00:00
f7fc802ec2 Se crea endpoint para procesar todos los pedimentos 2025-10-16 17:09:56 -05:00
50e35992db se quito stop 2025-10-16 09:47:20 -06:00
9a8827bb6f customs crear/update pedimento 2025-10-15 19:29:11 -06:00
6e0b7eaa91 Merge pull request 'feat: Mejorar endpoints de carga masiva de documentos' (#2) from feature/bulk-document-upload-nomenclatura into main
Reviewed-on: #2
2025-10-15 00:10:04 +00:00
9700d81dea se modifico tasks y views de auditor 2025-10-12 07:52:31 -06:00
8c842a6212 Merge pull request 'feat: agregar endpoints de eliminación masiva' (#1) from feature/bulk-delete-endpoints into main
Reviewed-on: #1
2025-10-10 01:34:32 +00:00
30 changed files with 5290 additions and 362 deletions

View File

@@ -17,4 +17,7 @@ EMAIL_HOST_PASSWORD=N036p7y!
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST=secure.emailsrvr.com EMAIL_HOST=secure.emailsrvr.com
SERVICE_API_URL=http://localhost:8001/api/v1 SERVICE_API_URL=http://host.docker.internal:8001/api/v1
SERVICE_API_URL_V2=http://host.docker.internal:8001/api/v2
CELERY_BROKER_URL=redis://redis_backend_dev:6379/0
CELERY_RESULT_BACKEND=redis://redis_backend_dev:6379/0

2
.gitignore vendored
View File

@@ -178,4 +178,4 @@ cython_debug/
#.idea/ #.idea/
# End of https://www.toptal.com/developers/gitignore/api/django # End of https://www.toptal.com/developers/gitignore/api/django
*.bak

View File

@@ -3,12 +3,17 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# Instalar dependencias del sistema necesarias
RUN apt-get update && apt-get install -y --no-install-recommends wget && \
wget https://www.rarlab.com/rar/rarlinux-x64-621.tar.gz && \
tar -xzvf rarlinux*.tar.gz && \
cp rar/unrar /usr/bin/unrar && \
rm -rf rarlinux*.tar.gz rar
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN pip install flower RUN pip install flower
COPY . . COPY . .

View File

@@ -3,8 +3,16 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# Instalar dependencias del sistema # Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \ # RUN apt-get update && apt-get install -y \
# supervisor \
# && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends \
supervisor \ supervisor \
wget \
&& wget https://www.rarlab.com/rar/rarlinux-x64-621.tar.gz \
&& tar -xzvf rarlinux*.tar.gz \
&& cp rar/unrar /usr/bin/unrar \
&& rm -rf rarlinux*.tar.gz rar \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copiar e instalar dependencias de Python # Copiar e instalar dependencias de Python

View File

@@ -1,6 +1,5 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from api.customs.tasks.auditoria import ( from api.customs.tasks.auditoria import (
auditar_procesamiento_remesas,
auditar_coves, auditar_coves,
auditar_acuse_cove, auditar_acuse_cove,
auditar_edocuments, auditar_edocuments,
@@ -15,7 +14,6 @@ class Command(BaseCommand):
# Definir las tareas disponibles # Definir las tareas disponibles
TAREAS_DISPONIBLES = { TAREAS_DISPONIBLES = {
'remesas': (auditar_procesamiento_remesas, "Auditoría de remesas"),
'partidas': (crear_partidas, "Creación de partidas"), 'partidas': (crear_partidas, "Creación de partidas"),
'coves': (auditar_coves, "Auditoría de COVEs"), 'coves': (auditar_coves, "Auditoría de COVEs"),
'acuse-cove': (auditar_acuse_cove, "Auditoría de acuses de COVEs"), 'acuse-cove': (auditar_acuse_cove, "Auditoría de acuses de COVEs"),

View File

@@ -0,0 +1,44 @@
from django.core.management.base import BaseCommand
from api.organization.models import Organizacion
from api.customs.tasks import microservice_v2
class Command(BaseCommand):
help = 'Ejecuta tareas de microservicio por organización y procesamiento.'
def add_arguments(self, parser):
parser.add_argument(
'--organizacion_id',
type=str,
help='ID de la organización a procesar (opcional, si no se envía se procesan todas)'
)
parser.add_argument(
'--procesamiento',
type=str,
help='Tipo de procesamiento a ejecutar (opcional, si no se envía se ejecutan todos)'
)
parser.add_argument(
'--todos',
type=bool,
help='Ejecutar todos los procesos (opcional)'
)
def handle(self, *args, **options):
todos = options.get('todos', False)
organizacion_id = options.get('organizacion_id')
procesamiento = options.get('procesamiento')
if todos:
organizaciones = Organizacion.objects.all()
for org in organizaciones:
microservice_v2.ejecutar_todos_por_organizacion(org.id)
self.stdout.write(self.style.SUCCESS('Se ejecutaron todos los procesos para todas las organizaciones.'))
return
if organizacion_id:
if procesamiento:
microservice_v2.ejecutar_procesamiento_por_organizacion(organizacion_id, procesamiento)
self.stdout.write(self.style.SUCCESS(f'Se ejecutó el procesamiento {procesamiento} para la organización {organizacion_id}.'))
else:
microservice_v2.ejecutar_todos_por_organizacion(organizacion_id)
self.stdout.write(self.style.SUCCESS(f'Se ejecutaron todos los procesos para la organización {organizacion_id}.'))

View File

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

View File

@@ -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
@@ -51,28 +54,6 @@ def auditor_descargas(pedimento, servicio, related_name, variable, mensaje):
print(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.") print(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.")
## Auditar pedimentos ## Auditar pedimentos
@shared_task
def auditar_procesamiento_remesas(organizacion_id):
pedimentos = obtener_pedimentos(organizacion_id)
for pedimento in pedimentos:
if pedimento.remesas:
# Tipo 3: Remesa
if not pedimento.documents.filter(document_type=3).exists():
ProcesamientoPedimento.objects.get_or_create(
pedimento=pedimento,
servicio_id=5, # ID del servicio de remesas
organizacion=organizacion_id
)
else:
xml_data = extraer_coves(pedimento)
if xml_data:
for remesa in xml_data:
Cove.objects.get_or_create(
pedimento=pedimento,
numero_cove=remesa.get('remesaSA'),
organizacion=organizacion_id
)
@shared_task @shared_task
def auditar_procesamiento_remesa_por_pedimento(pedimento_id): def auditar_procesamiento_remesa_por_pedimento(pedimento_id):
@@ -220,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'
) )
@@ -259,4 +240,219 @@ def auditar_acuse(organizacion_id):
variable='acuse_descargado', variable='acuse_descargado',
mensaje='acuse' mensaje='acuse'
) )
@shared_task
def auditar_cove_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=8,
related_name='coves',
variable='cove_descargado',
mensaje='COVE'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_acuse_cove_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=9,
related_name='coves',
variable='acuse_cove_descargado',
mensaje='acuse de COVE'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_edocument_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=7,
related_name='documentos',
variable='edocument_descargado',
mensaje='EDocument'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_acuse_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=6,
related_name='documentos',
variable='acuse_descargado',
mensaje='acuse'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
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

@@ -265,7 +265,6 @@ def procesar_remesas(organizacion_id):
print(f"Servicio enviado para pedimento {pedimento.pedimento}") print(f"Servicio enviado para pedimento {pedimento.pedimento}")
@shared_task @shared_task
def procesar_coves(organizacion_id): def procesar_coves(organizacion_id):
pedimentos = Pedimento.objects.filter( pedimentos = Pedimento.objects.filter(
@@ -428,4 +427,35 @@ def documentos_con_errores(organizacion_id):
print(f"Documento con error: {doc.id} en organización {organizacion_id}") print(f"Documento con error: {doc.id} en organización {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.
# documentos = Document.objects.all() --- IGNORE ---
def ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento):
if procesamiento == 'coves':
procesar_coves.delay(organizacion_id)
elif procesamiento == 'edocs':
procesar_edocs.delay(organizacion_id)
elif procesamiento == 'acuses':
procesar_acuses.delay(organizacion_id)
elif procesamiento == 'acuse_coves':
procesar_acuse_coves.delay(organizacion_id)
elif procesamiento == 'partidas':
procesar_partidas.delay(organizacion_id)
elif procesamiento == 'pedimentos_completos':
procesar_pedimentos_completos.delay(organizacion_id)
elif procesamiento == 'remesas':
procesar_remesas.delay(organizacion_id)
else:
# Procesamiento no reconocido
pass
def ejecutar_todos_por_organizacion(organizacion_id):
procesar_coves.delay(organizacion_id)
procesar_edocs.delay(organizacion_id)
procesar_acuses.delay(organizacion_id)
procesar_acuse_coves.delay(organizacion_id)
procesar_partidas.delay(organizacion_id)
procesar_pedimentos_completos.delay(organizacion_id)
procesar_remesas.delay(organizacion_id)

View File

@@ -34,8 +34,32 @@ from .views_auditor import (
crear_partidas_organizacion, crear_partidas_organizacion,
crear_partidas_pedimento, crear_partidas_pedimento,
auditar_pedimentos_endpoint, auditar_pedimentos_endpoint,
auditar_procesamiento_remesas_endpoint, auditar_coves_endpoint,
auditar_procesamiento_remesa_pedimento_endpoint auditar_acuse_cove_endpoint,
auditar_edocuments_endpoint,
auditar_acuse_endpoint,
auditar_cove_pedimento_endpoint,
auditar_acuse_cove_pedimento_endpoint,
auditar_edocument_pedimento_endpoint,
auditar_acuse_pedimento_endpoint,
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 = [
@@ -43,6 +67,32 @@ urlpatterns = [
path('auditor/crear-partidas/organizacion/', crear_partidas_organizacion, name='crear-partidas-organizacion'), path('auditor/crear-partidas/organizacion/', crear_partidas_organizacion, name='crear-partidas-organizacion'),
path('auditor/crear-partidas/pedimento/', crear_partidas_pedimento, name='crear-partidas-pedimento'), path('auditor/crear-partidas/pedimento/', crear_partidas_pedimento, name='crear-partidas-pedimento'),
path('auditor/auditar-pedimentos/', auditar_pedimentos_endpoint, name='auditar-pedimentos'), path('auditor/auditar-pedimentos/', auditar_pedimentos_endpoint, name='auditar-pedimentos'),
path('auditor/auditar-procesamiento-remesas/', auditar_procesamiento_remesas_endpoint, name='auditar-procesamiento-remesas'), path('auditor/auditar-coves/', auditar_coves_endpoint, name='auditar-coves'),
path('auditor/auditar-procesamiento-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-procesamiento-remesa-pedimento'), path('auditor/auditar-acuse-cove/', auditar_acuse_cove_endpoint, name='auditar-acuse-cove'),
path('auditor/auditar-edocuments/', auditar_edocuments_endpoint, name='auditar-edocuments'),
path('auditor/auditar-acuse/', auditar_acuse_endpoint, name='auditar-acuse'),
path('auditor/auditar-cove/pedimento/', auditar_cove_pedimento_endpoint, name='auditar-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-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/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

@@ -23,7 +23,7 @@ from api.customs.models import (
EDocument, EDocument,
Cove, Cove,
Importador, Importador,
Partida Partida,
) )
from api.customs.serializers import ( from api.customs.serializers import (
PedimentoSerializer, PedimentoSerializer,
@@ -43,11 +43,12 @@ import re
import zipfile import zipfile
import tempfile import tempfile
import shutil import shutil
from datetime import datetime import subprocess
from datetime import date, datetime
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.parsers import MultiPartParser, FormParser
from api.record.models import Document, DocumentType from api.record.models import Document, DocumentType, Fuente
# Importar rarfile de manera opcional # Importar rarfile de manera opcional
try: try:
@@ -56,6 +57,116 @@ 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():
"""
Devuelve lista de extractores disponibles en orden de preferencia
"""
extractors = []
if RAR_SUPPORT:
extractors.append('rarfile')
# Verificar si 'unrar' está disponible
if shutil.which('unrar'):
extractors.append('unrar')
# Verificar si '7z' o '7za' están disponibles
if shutil.which('7z'):
extractors.append('7z')
elif shutil.which('7za'):
extractors.append('7za')
return extractors
def extract_rar_to_dir(rar_path, dest_dir):
"""
Extrae un archivo RAR a `dest_dir` usando varios mecanismos de respaldo:
1) intentará usar la librería `rarfile` si está disponible,
2) intentará ejecutar la utilidad `unrar` si está instalada en el sistema,
3) intentará ejecutar `7z`/`7za` (p7zip) si está instalada.
Lanza Exception con mensaje explicativo si falla.
"""
# Versión que primero verifica herramientas disponibles
available = get_available_extractors()
if not available:
raise Exception("No hay herramientas de extracción disponibles.")
print(f"Extractores disponibles (en orden de preferencia): {available}")
# Intento con rarfile primero si está disponible
# if RAR_SUPPORT:
if 'rarfile' in available and RAR_SUPPORT:
try:
# rarfile puede trabajar con rutas en disco mejor que con file-like
with rarfile.RarFile(rar_path) as rf:
rf.extractall(dest_dir)
try:
if os.path.exists(rar_path):
os.remove(rar_path)
print(f"Archivo original eliminado: {rar_path}")
except OSError as remove_error:
print(f"Advertencia: No se pudo eliminar '{rar_path}': {remove_error}")
return
except Exception as e:
# Si rarfile falla (por ejemplo RarCannotExec), seguimos con herramientas externas
# Hacer log para depuración
print(f"rarfile extraction failed, will try external tools: {e}")
# Intento con comandos externos
# Probar 'unrar' primero
external_cmds = [
['unrar', 'x', '-o+', rar_path, dest_dir],
['7z', 'x', rar_path, f'-o{dest_dir}', '-y'],
['7za', 'x', rar_path, f'-o{dest_dir}', '-y']
]
# for cmd in external_cmds:
# try:
# subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# try:
# if os.path.exists(rar_path):
# os.remove(rar_path)
# print(f"Archivo original eliminado: {rar_path}")
# except OSError as remove_error:
# print(f"Advertencia: No se pudo eliminar '{rar_path}': {remove_error}")
# return
# except FileNotFoundError:
# # El ejecutable no existe en PATH, intentar siguiente
# continue
# except subprocess.CalledProcessError as e:
# # El comando falló en la extracción; intentar siguiente
# print(f"External extractor failed ({cmd[0]}): {e}")
# continue
for extractor_name in available:
if extractor_name in external_cmds:
cmd = external_cmds[extractor_name]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
if os.path.exists(rar_path):
os.remove(rar_path)
print(f"Archivo original eliminado: {rar_path}")
except OSError as remove_error:
print(f"Advertencia: No se pudo eliminar '{rar_path}': {remove_error}")
return
except FileNotFoundError:
# El ejecutable no existe en PATH, intentar siguiente
continue
except subprocess.CalledProcessError as e:
# El comando falló en la extracción; intentar siguiente
print(f"External extractor {extractor_name} failed ({cmd[0]}): {e}")
continue
# Si llegamos aquí, ningún método funcionó
raise Exception("No se encontró una herramienta válida para extraer RAR (rarfile sin backend, 'unrar' o '7z' no disponibles o extracción fallida). Instale 'unrar' o 'p7zip' y asegúrese de que estén en PATH, o configure rarfile con un backend.")
from .tasks.microservice_v2 import * from .tasks.microservice_v2 import *
from .tasks.auditoria import crear_partidas_por_pedimento from .tasks.auditoria import crear_partidas_por_pedimento
@@ -168,9 +279,21 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app'] filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app']
search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento'] search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento']
# AGREGAR ESTOS CAMPOS PARA ORDENACIÓN
ordering_fields = ['created_at', 'pedimento', 'fecha_pago', 'aduana', 'patente']
ordering = ['-created_at'] # Orden descendente por fecha de creación por defecto
def get_queryset(self): def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
# pedimento_app_filter = self.request.GET.get('pedimento_app', None)
# if pedimento_app_filter:
# print(f"Filtro por pedimento_app: {pedimento_app_filter}")
# queryset = queryset.filter(pedimento_app__icontains=pedimento_app_filter)
return queryset
def perform_create(self, serializer): def perform_create(self, serializer):
""" """
@@ -194,24 +317,24 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Verificar si la respuesta fue exitosa # Verificar si la respuesta fue exitosa
if response.status_code == 200: if response.status_code == 200:
print(f"Servicio FastAPI ejecutado exitosamente: {response.status_code}") print(f"Servicio FastAPI ejecutado exitosamente: {response.status_code}")
print(f"📄 Respuesta: {response.json()}") print(f"Respuesta: {response.json()}")
elif response.status_code == 201: elif response.status_code == 201:
print(f"Recurso creado exitosamente en FastAPI: {response.status_code}") print(f"Recurso creado exitosamente en FastAPI: {response.status_code}")
print(f"📄 Respuesta: {response.json()}") print(f"Respuesta: {response.json()}")
else: else:
print(f"⚠️ Servicio FastAPI respondió con error: {response.status_code}") print(f"Servicio FastAPI respondió con error: {response.status_code}")
print(f"📄 Respuesta: {response.text}") print(f"Respuesta: {response.text}")
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
print(f"No se pudo conectar al servicio FastAPI: {e}") print(f"No se pudo conectar al servicio FastAPI: {e}")
print(f"🔧 Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}") print(f"Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}")
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
print(f"Timeout al conectar con el servicio FastAPI: {e}") print(f"Timeout al conectar con el servicio FastAPI: {e}")
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"🚨 Error de request al servicio FastAPI: {e}") print(f"Error de request al servicio FastAPI: {e}")
except Exception as e: except Exception as e:
print(f"💥 Error inesperado al llamar al servicio FastAPI: {e}") print(f"Error inesperado al llamar al servicio FastAPI: {e}")
def perform_update(self, serializer): def perform_update(self, serializer):
""" """
@@ -249,6 +372,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):
""" """
@@ -400,6 +542,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Regex para validar nomenclatura: anio-aduana-patente-pedimento # Regex para validar nomenclatura: anio-aduana-patente-pedimento
nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$') nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$')
nomenclatura_pattern_sin_anio = re.compile(r'^(\d{2,3})-(\d{4})-(\d{7})$')
created_pedimentos = [] created_pedimentos = []
failed_files = [] failed_files = []
@@ -478,21 +621,38 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
elif archivo_name.endswith('.rar'): elif archivo_name.endswith('.rar'):
# Manejar archivo RAR # Manejar archivo RAR: guardar el archivo en disco y usar helper con fallbacks
if not RAR_SUPPORT: # Guardar el archivo subido en un path temporal dentro del sub_dir
return Response( archivo_temp_path = os.path.join(sub_dir, archivo.name)
{"error": "Soporte para archivos RAR no disponible. Instalar rarfile: pip install rarfile"}, with open(archivo_temp_path, 'wb') as f:
status=status.HTTP_400_BAD_REQUEST for chunk in archivo.chunks():
) f.write(chunk)
try: try:
with rarfile.RarFile(archivo, 'r') as rar_ref: extract_rar_to_dir(archivo_temp_path, sub_dir)
rar_ref.extractall(sub_dir) print(f"Archivo RAR {archivo.name} extraído en {sub_dir}")
print(f"Archivo RAR {archivo.name} extraído en sub_dir") except Exception as e:
except rarfile.Error 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( return Response(
{"error": f"Error al extraer archivo RAR {archivo.name}: {str(e)}"}, {"error": f"Error al extraer archivo RAR {archivo.name}: {error_msg}. {help_msg}"},
status=status.HTTP_400_BAD_REQUEST 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: else:
# Asumir que es un archivo individual # Asumir que es un archivo individual
# Crear el archivo en el subdirectorio # Crear el archivo en el subdirectorio
@@ -530,7 +690,9 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Validar nomenclatura # Validar nomenclatura
match = nomenclatura_pattern.match(folder_name) match = nomenclatura_pattern.match(folder_name)
if not match: match_sin_anio = nomenclatura_pattern_sin_anio.match(folder_name)
if not match and not match_sin_anio:
print(f"Nomenclatura inválida: {folder_name}") print(f"Nomenclatura inválida: {folder_name}")
# Determinar el archivo original basado en el subdirectorio # Determinar el archivo original basado en el subdirectorio
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
@@ -541,25 +703,60 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
}) })
continue continue
print(f"Nomenclatura válida: {folder_name}") if match:
anio, aduana, patente, pedimento_num = match.groups()
print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}") print(f"Nomenclatura válida: {folder_name}")
anio, aduana, patente, pedimento_num = match.groups()
# Crear fecha_pago basada en el año print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
try: # Formato original: anio-aduana-patente-pedimento
# Convertir año de 2 dígitos a 4 dígitos # Crear fecha_pago basada en el año
anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio) try:
fecha_pago = datetime(anio_completo, 1, 1).date() # Convertir año de 2 dígitos a 4 dígitos
print(f"Fecha de pago calculada: {fecha_pago}") anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio)
except ValueError: fecha_pago = datetime(anio_completo, 1, 1).date()
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar') print(f"Fecha de pago calculada: {fecha_pago}")
failed_files.append({ except ValueError:
"file": relative_path, archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
"archivo_original": archivo_original, failed_files.append({
"error": f"Año inválido: {anio}" "file": relative_path,
}) "archivo_original": archivo_original,
continue "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 # 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}")
@@ -567,7 +764,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Verificar si el pedimento ya existe # Verificar si el pedimento ya existe
existing_pedimento = Pedimento.objects.filter( existing_pedimento = Pedimento.objects.filter(
pedimento_app=pedimento_app, pedimento_app=pedimento_app,
organizacion=organizacion # organizacion=organizacion
).first() ).first()
print(f"Pedimento existente: {existing_pedimento is not None}") print(f"Pedimento existente: {existing_pedimento is not None}")
@@ -639,12 +836,32 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Crear ContentFile que Django puede manejar correctamente # Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name) django_file = ContentFile(file_content, name=file_name)
# # Verificar si el documento ya existe para este pedimento y archivo
# print("🔍 Verificando existencia previa del documento...")
# # Reemplazar múltiples caracteres
# normalized_file_name = file_name.replace(" ", "_")
# file_name_without_extension = normalized_file_name.rsplit('.', 1)[0]
# extension_file = os.path.splitext(normalized_file_name)[1].lower().lstrip('.')
# existing_document = Document.objects.filter(
# pedimento_id=pedimento.id,
# archivo__contains=file_name_without_extension,
# extension=extension_file
# ).first()
# if existing_document:
# print(f"Documento existente encontrado, omitiendo creación: ID {existing_document.id}")
# continue
print(f"Creando documento para archivo: {file_name}") print(f"Creando documento para archivo: {file_name}")
# Crear documento - Django automáticamente guardará el archivo en media/documents/ # Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create( document = Document.objects.create(
organizacion=organizacion, organizacion=organizacion,
pedimento_id=pedimento.id, pedimento_id=pedimento.id,
document_type=document_type, document_type=document_type,
fuente_id=4, # Fuente: Carga Plataforma
archivo=django_file, archivo=django_file,
size=len(file_content), size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.') extension=os.path.splitext(file_name)[1].lower().lstrip('.')
@@ -697,6 +914,549 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
@action(detail=False, methods=['post'], url_path='bulk-create-pedimento_desk', parser_classes=[MultiPartParser, FormParser])
def bulk_create_pedimento_desk(self, request):
"""
Endpoint para crear múltiples pedimentos desde EFC APP Desk.
FormData esperado:
- contribuyente: string (nombre del contribuyente)
- archivos: files (pueden ser múltiples archivos: zip, rar o individuales)
Nomenclatura esperada de archivos: anio-aduana-patente-pedimento
- anio: 2 dígitos (ej: 24)
- aduana: 2 o 3 dígitos (ej: 01, 123)
- patente: 4 dígitos (ej: 3420)
- pedimento: 7 dígitos (ej: 1234567)
Ejemplo: 24-01-3420-1234567
Nota: Cada archivo ZIP/RAR se procesa independientemente en su propio subdirectorio.
Respuesta exitosa:
{
"message": "Pedimentos creados exitosamente",
"created_count": 5,
"created_pedimentos": [...],
"documents_created": 15,
"processed_files": 3,
"summary": "Procesados 3 archivo(s): 5 pedimento(s) creado(s), 15 documento(s) asociado(s)",
"failed_files": [],
"errors": []
}
"""
print(request.data)
# Validar datos requeridos
contribuyente = request.data.get('contribuyente')
fecha_pago_input = request.data.get('fecha_pago')
clave_pedimento_input = request.data.get('clave_pedimento')
patente_input = request.data.get('patente')
tipo_operacion_input = request.data.get('tipo_operacion')
aduana_input = request.data.get('aduana')
contribuyente_input = request.data.get('contribuyente')
curp_apoderado_input = request.data.get('curp_apoderado')
partidas_input = request.data.get('partidas')
fuente_archivos = request.data.get('partidas')
archivos = request.FILES.getlist('archivos')
# if not contribuyente:
# return Response(
# {"error": "Se requiere el campo 'contribuyente'"},
# status=status.HTTP_400_BAD_REQUEST
# )
if not archivos:
return Response(
{
"tieneError": True,
"error": "Se requiere al menos un archivo"
},
status=status.HTTP_400_BAD_REQUEST
)
# Validar organización del usuario
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
return Response(
{
"tieneError": True,
"error": "Usuario no autenticado o sin organización"
},
status=status.HTTP_400_BAD_REQUEST
)
organizacion = request.user.organizacion
# Regex para validar nomenclatura: anio-aduana-patente-pedimento
nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$')
nomenclatura_pattern_sin_anio = re.compile(r'^(\d{2,3})-(\d{4})-(\d{7})$')
created_pedimentos = []
failed_files = []
errors = []
documents_created = 0
temp_dir = None
# Obtener DocumentType ANTES de la transacción atómica
# print("Intentando obtener o crear DocumentType...")
try:
# Primero intentar obtener si ya existe
try:
document_type = DocumentType.objects.get(nombre="Pedimento")
# print(f"DocumentType obtenido existente: {document_type.nombre} (ID: {document_type.id})")
except DocumentType.DoesNotExist:
# Si no existe, crear uno nuevo
document_type = DocumentType.objects.create(
nombre="Pedimento",
descripcion="Documento de pedimento"
)
# print(f"DocumentType creado nuevo: {document_type.nombre} (ID: {document_type.id})")
except Exception as e:
# print(f"Error al obtener/crear DocumentType: {str(e)}")
# Como fallback, intentar obtener cualquier DocumentType existente
try:
# document_type = DocumentType.objects.first()
# if document_type:
# print(f"Usando DocumentType existente como fallback: {document_type.nombre} (ID: {document_type.id})")
# else:
# print("No hay DocumentType disponible")
# return Response(
# {"error": "No se pudo configurar el tipo de documento y no hay tipos existentes"},
# status=status.HTTP_500_INTERNAL_SERVER_ERROR
# )
document_type = DocumentType.objects.first()
if not document_type:
return Response(
{"error": "No se pudo configurar el tipo de documento y no hay tipos existentes"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except Exception as fallback_error:
# print(f"Error en fallback: {str(fallback_error)}")
return Response(
{
"tieneError": True,
"error": f"Error crítico al configurar tipo de documento: {str(e)}"
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
try:
# print("Iniciando transacción atómica...")
with transaction.atomic():
# Crear directorio temporal
temp_dir = tempfile.mkdtemp()
# print(f"Directorio temporal creado: {temp_dir}")
# Procesar cada archivo enviado
for idx, archivo in enumerate(archivos):
archivo_name = archivo.name.lower()
# print(f"Procesando archivo {idx + 1}/{len(archivos)}: {archivo_name}")
# Crear subdirectorio para cada archivo usando el nombre del archivo sin extensión
archivo_name_sin_extension = os.path.splitext(archivo.name)[0]
sub_dir = os.path.join(temp_dir, archivo_name_sin_extension)
os.makedirs(sub_dir, exist_ok=True)
# print(f"Subdirectorio creado: {sub_dir}")
if archivo_name.endswith('.zip'):
# Manejar archivo ZIP
# print("Es un archivo ZIP")
try:
with zipfile.ZipFile(archivo, 'r') as zip_ref:
zip_ref.extractall(sub_dir)
# print("Archivo ZIP extraído exitosamente")
except zipfile.BadZipFile as e:
return Response(
{
"tieneError": True,
"error": f"Archivo ZIP corrupto o inválido: {archivo.name} - {str(e)}"
},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
return Response(
{
"tieneError": True,
"error": f"Error al extraer ZIP {archivo.name}: {str(e)}"
},
status=status.HTTP_400_BAD_REQUEST
)
elif archivo_name.endswith('.rar'):
# Manejar archivo RAR: guardar el archivo en disco y usar helper con fallbacks
# Guardar el archivo subido en un path temporal dentro del sub_dir
archivo_temp_path = os.path.join(sub_dir, archivo.name)
with open(archivo_temp_path, 'wb') as f:
for chunk in archivo.chunks():
f.write(chunk)
try:
extract_rar_to_dir(archivo_temp_path, sub_dir)
# print(f"Archivo RAR {archivo.name} extraído en {sub_dir}")
except Exception as e:
error_msg = str(e)
help_msg = "Instale 'unrar' o 'p7zip' (7z) y asegúrese de que estén en PATH, o instale y configure 'rarfile' con un backend."
return Response(
{
"tieneError": True,
"error": f"Error al extraer archivo RAR {archivo.name}: {error_msg}. {help_msg}"
},
status=status.HTTP_400_BAD_REQUEST
)
else:
# Asumir que es un archivo individual
# Crear el archivo en el subdirectorio
archivo_path = os.path.join(sub_dir, archivo.name)
with open(archivo_path, 'wb') as f:
for chunk in archivo.chunks():
f.write(chunk)
# print(f"Archivo individual {archivo.name} guardado en sub_dir:", archivo_path)
# Recorrer todos los archivos extraídos o el directorio
print("Iniciando recorrido de archivos...")
for root, dirs, files in os.walk(temp_dir):
# print(f"Revisando directorio: {root}")
# print(f"Archivos encontrados: {files}")
for file_name in files:
# print(f"Procesando archivo: {file_name}")
file_path = os.path.join(root, file_name)
# Obtener la ruta relativa para determinar la estructura de carpetas
relative_path = os.path.relpath(file_path, temp_dir)
# print(f"Ruta relativa: {relative_path}")
# Determinar si el archivo está en una carpeta que sigue la nomenclatura
folder_name = None
if os.path.dirname(relative_path):
# El archivo está dentro de una carpeta
folder_parts = relative_path.split(os.sep)
folder_name = folder_parts[0] # Primera carpeta (nombre del archivo ZIP/RAR sin extensión)
else:
# El archivo está en la raíz, usar el nombre del archivo sin extensión
folder_name = os.path.splitext(file_name)[0]
# print(f"Folder name para validación: {folder_name}")
# Validar nomenclatura
match = nomenclatura_pattern.match(folder_name)
match_sin_anio = nomenclatura_pattern_sin_anio.match(folder_name)
if not match and not match_sin_anio:
# print(f"Nomenclatura inválida: {folder_name}")
# Determinar el archivo original basado en el subdirectorio
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Nomenclatura inválida: {folder_name}. Esperado: anio-aduana-patente-pedimento"
})
continue
if match:
# print(f"Nomenclatura válida: {folder_name}")
anio, aduana, patente, pedimento_num = match.groups()
# print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
# Formato original: anio-aduana-patente-pedimento
# Crear fecha_pago basada en el año
try:
# Convertir año de 2 dígitos a 4 dígitos
anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio)
fecha_pago = datetime(anio_completo, 1, 1).date()
# print(f"Fecha de pago calculada: {fecha_pago}")
except ValueError:
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Año inválido: {anio}"
})
continue
elif match_sin_anio:
# print(f"Nomenclatura válida sin año: {folder_name}")
# Formato sin año: aduana-patente-pedimento
aduana, patente, pedimento_num = match_sin_anio.groups()
# print(f"Extraído - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
# Obtener el primer dígito del pedimento
primer_digito_pedimento = int(pedimento_num[0]) if pedimento_num else 0
# Usar año actual para fecha_pago y ajustar según el dígito del pedimento
año_actual = datetime.now().year
# Crear año con el dígito del pedimento (reemplazando el último dígito)
año_con_digito = int(str(año_actual)[:-1] + str(primer_digito_pedimento))
# Aplicar lógica de comparación
if año_con_digito <= año_actual:
# Si el año con dígito es menor o igual al año actual
año_final = año_con_digito
else:
# Si el año con dígito es mayor al año actual, restar 10
año_final = año_con_digito - 10
# Tomar los últimos 2 dígitos del año final
anio = año_final % 100
# Crear fecha de pago (primer día del año)
fecha_pago = datetime(año_final , 1, 1).date()
# print(f"Fecha de pago (año actual) calculada: {fecha_pago}")
# Generar pedimento_app
pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}"
# print(f"Pedimento_app generado: {pedimento_app}")
# print(f"Buscando pedimento existente con pedimento_app: {pedimento_app} y organización ID: {organizacion.id}")
# Verificar si el pedimento ya existe
existing_pedimento = Pedimento.objects.filter(
pedimento=int(pedimento_num),
# pedimento_app=pedimento_app,
organizacion=organizacion
).first()
# print(f"Pedimento existente: {existing_pedimento is not None}")
if not existing_pedimento:
# print("📝 Pedimento no existe, creando nuevo...")
# Crear nuevo pedimento
try:
# print("🔄 Iniciando creación de pedimento...")
importador = None
if contribuyente:
# Obtener o crear el importador
# print(f"🏢 Buscando/creando importador con RFC: {contribuyente}")
importador, created = Importador.objects.get_or_create(
rfc=contribuyente,
defaults={
'nombre': f"Importador {contribuyente}",
'organizacion': organizacion
}
)
# if created:
# print(f"✅ Importador creado: {importador.rfc} - {importador.nombre}")
# else:
# print(f"♻️ Importador existente: {importador.rfc} - {importador.nombre}")
if tipo_operacion_input:
tipo_operacion_input = TipoOperacion.objects.get(id=tipo_operacion_input)
pedimento = Pedimento.objects.create(
organizacion=organizacion,
contribuyente=importador if importador else None,
pedimento=int(pedimento_num),
aduana=int(aduana),
patente=int(patente),
fecha_pago=fecha_pago_input if fecha_pago_input else fecha_pago,
curp_apoderado=curp_apoderado_input if curp_apoderado_input else "",
numero_partidas=partidas_input if partidas_input else 0,
tipo_operacion=tipo_operacion_input if tipo_operacion_input else None,
pedimento_app=pedimento_app,
agente_aduanal=f"Agente {patente}", # Valor por defecto
clave_pedimento=clave_pedimento_input if clave_pedimento_input else "A1" # Valor por defecto
)
# print(f"✅ Pedimento creado exitosamente: ID {pedimento.id}, pedimento_app: {pedimento_app}")
created_pedimentos.append({
"id": str(pedimento.id),
"pedimento_app": pedimento_app,
"contribuyente": getattr(importador, 'rfc', None),
"contribuyente_nombre": getattr(importador, 'nombre', None)
})
except Exception as e:
# print(f"❌ Error al crear pedimento: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Error al crear pedimento: {str(e)}"
})
continue
else:
# print(f"♻️ Usando pedimento existente: ID {existing_pedimento.id}")
# Usar pedimento existente
# # Actualizar Importador
if contribuyente:
importador, created = Importador.objects.get_or_create(
rfc=contribuyente,
defaults={
'nombre': f"Importador {contribuyente}",
'organizacion': organizacion
}
)
importador_db = existing_pedimento.contribuyente
if importador_db:
if importador_db != importador:
existing_pedimento.contribuyente = importador
else:
existing_pedimento.contribuyente = importador
existing_pedimento.save()
# Actualizar Tipo Operacion
if tipo_operacion_input:
tipo_operacion_input = TipoOperacion.objects.get(id=tipo_operacion_input)
if tipo_operacion_input:
tipo_operacion_db = existing_pedimento.tipo_operacion
if not tipo_operacion_db:
existing_pedimento.tipo_operacion = tipo_operacion_input
existing_pedimento.save()
# Actualizar fecha de pago solo cuando aun no esta actualizado
if fecha_pago_input:
fecha_db = existing_pedimento.fecha_pago
# Verificar si hay fecha en BD
if fecha_db:
# Asegurar que trabajamos con date
if isinstance(fecha_db, datetime):
fecha_db = fecha_db.date()
# Si la fecha existe y es 1 de enero, actualizar
if fecha_db.month == 1 and fecha_db.day == 1:
# Actualizar Fecha
existing_pedimento.fecha_pago = fecha_pago_input
existing_pedimento.save()
else:
existing_pedimento.fecha_pago = fecha_pago_input
existing_pedimento.save()
if clave_pedimento_input:
clavePedimento = existing_pedimento.clave_pedimento
if not clavePedimento:
existing_pedimento.clave_pedimento = clave_pedimento_input
existing_pedimento.save()
if curp_apoderado_input:
curp = existing_pedimento.curp_apoderado
if not curp:
existing_pedimento.curp_apoderado = curp_apoderado_input
existing_pedimento.save()
if partidas_input:
numPartidas = existing_pedimento.numero_partidas
if not numPartidas:
existing_pedimento.numero_partidas = partidas_input
existing_pedimento.save()
elif numPartidas <= 0:
existing_pedimento.numero_partidas = partidas_input
existing_pedimento.save()
pedimento = existing_pedimento
# print(f"🔄 Iniciando creación de documento para pedimento ID: {pedimento.id}")
# Crear documento asociado al pedimento
try:
# print("📖 Leyendo archivo desde directorio temporal...")
# Leer el archivo desde el directorio temporal
with open(file_path, 'rb') as f:
file_content = f.read()
# print(f"📄 Archivo leído: {len(file_content)} bytes")
# Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name)
# # Verificar si el documento ya existe para este pedimento y archivo
# print("🔍 Verificando existencia previa del documento...")
# # Reemplazar múltiples caracteres
# normalized_file_name = file_name.replace(" ", "_")
# file_name_without_extension = normalized_file_name.rsplit('.', 1)[0]
# extension_file = os.path.splitext(normalized_file_name)[1].lower().lstrip('.')
# existing_document = Document.objects.filter(
# pedimento_id=pedimento.id,
# archivo__contains=file_name_without_extension,
# extension=extension_file
# ).first()
# if existing_document:
# print(f"Documento existente encontrado, omitiendo creación: ID {existing_document.id}")
# continue
# try:
# fuente = Fuente.objects.get(nombre="APP-EFC")
# except Fuente.DoesNotExist:
# fuente = Fuente.objects.create(
# nombre="APP-EFC",
# descripcion='Transmitido por la app de escritorio'
# )
fuente, created = Fuente.objects.get_or_create(
nombre="APP-EFC",
descripcion='Transmitido por la app de escritorio'
)
# print(f"Creando documento para archivo: {file_name}")
# Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento.id,
document_type=document_type,
fuente_id=fuente.id,
archivo=django_file,
size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.')
)
# print(f"Documento creado exitosamente: {document.id}")
documents_created += 1
# print(f"📊 Total documentos creados hasta ahora: {documents_created}")
except Exception as e:
# print(f"❌ Error al crear documento: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Error al crear documento: {str(e)}"
})
continue
# print(f"🏁 Procesamiento completado. Archivos procesados en este directorio.")
except Exception as e:
return Response(
{ "tieneError": True,
"error": f"Error durante el procesamiento: {str(e)}"
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
finally:
# Limpiar directorio temporal
if temp_dir and os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
# Preparar respuesta
response_data = {
"tieneError": False,
"failed_files": failed_files,
"processed_files": len(archivos),
}
if failed_files:
response_data["tieneError"] = True
response_data.update({
"message": "Procesamiento completado con algunos errores",
"errors": [item["error"] for item in failed_files]
})
response_status = status.HTTP_207_MULTI_STATUS
else:
response_data["message"] = "Pedimentos creados exitosamente"
response_status = status.HTTP_201_CREATED
return Response(response_data, status=response_status)
my_tags = ['Pedimentos'] my_tags = ['Pedimentos']
class PartidaViewSet(viewsets.ModelViewSet): class PartidaViewSet(viewsets.ModelViewSet):

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

@@ -4,12 +4,22 @@ from rest_framework.routers import DefaultRouter
# import necessary viewsets # import necessary viewsets
# from .views import YourViewSet # Import your viewsets here # from .views import YourViewSet # Import your viewsets here
from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView from .views import (DocumentViewSet
, ProtectedDocumentDownloadView
, BulkDownloadZipView
, GetFuenteView
, DocumentTypeView
, ExpedienteZipDownloadView
, MultiPedimentoZipDownloadView
, PedimentoDocumentViewSet
, TriggerPedimentoCompletoView)
# Create a router and register your viewsets with it # Create a router and register your viewsets with it
router = DefaultRouter() router = DefaultRouter()
# Register your viewsets with the router here # Register your viewsets with the router he -fre
# Example: # Example:
# from .views import MyViewSet # from .views import MyViewSet
# router.register(r'myviewset', MyViewSet, basename='myviewset') # router.register(r'myviewset', MyViewSet, basename='myviewset')
@@ -23,5 +33,9 @@ urlpatterns = [
path('documents/descargar/<uuid:pk>/', ProtectedDocumentDownloadView.as_view(), name='descargar-documento'), path('documents/descargar/<uuid:pk>/', ProtectedDocumentDownloadView.as_view(), name='descargar-documento'),
path('fuente/', GetFuenteView.as_view(), name='get-fuente'), path('fuente/', GetFuenteView.as_view(), name='get-fuente'),
path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'), path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'),
path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'),
path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'),
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

@@ -0,0 +1,30 @@
# Generated by Django 5.2.3 on 2025-10-21 23:56
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ReportDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filters', models.JSONField(blank=True, null=True)),
('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'Procesando'), ('ready', 'Listo'), ('error', 'Error')], default='pending', max_length=20)),
('file', models.FileField(blank=True, null=True, upload_to='reports/')),
('error_message', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('finished_at', models.DateTimeField(blank=True, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='report_documents', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

View File

@@ -1,3 +1,21 @@
from django.db import models
# Create your models here. from django.db import models
from django.contrib.auth import get_user_model
class ReportDocument(models.Model):
STATUS_CHOICES = [
('pending', 'Pendiente'),
('processing', 'Procesando'),
('ready', 'Listo'),
('error', 'Error'),
]
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='report_documents')
filters = models.JSONField(blank=True, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
file = models.FileField(upload_to='reports/', blank=True, null=True)
error_message = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
finished_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return f"Reporte {self.id} - {self.status}"

View File

@@ -0,0 +1,85 @@
from celery import shared_task
from django.core.files.base import ContentFile
from django.utils import timezone
from api.reports.models import ReportDocument
from api.customs.models import Pedimento, Cove, EDocument, Partida
from django.db.models import Q
import csv
import os
from django.conf import settings
@shared_task
def generate_report_document(report_id):
try:
report = ReportDocument.objects.get(id=report_id)
report.status = 'processing'
report.save(update_fields=['status'])
filters = report.filters or {}
# Construir Q para filtros complejos
pedimentos_filters = Q()
if filters.get('organizacion_id'):
pedimentos_filters &= Q(organizacion_id=filters['organizacion_id'])
if filters.get('fecha_pago__gte'):
pedimentos_filters &= Q(fecha_pago__gte=filters['fecha_pago__gte'])
if filters.get('fecha_pago__lte'):
pedimentos_filters &= Q(fecha_pago__lte=filters['fecha_pago__lte'])
if filters.get('contribuyente__rfc'):
pedimentos_filters &= Q(contribuyente__rfc=filters['contribuyente__rfc'])
if filters.get('patente'):
pedimentos_filters &= Q(patente=filters['patente'])
if filters.get('aduana'):
pedimentos_filters &= Q(aduana=filters['aduana'])
if filters.get('pedimento'):
pedimentos_filters &= Q(pedimento=filters['pedimento'])
if filters.get('pedimento_app'):
pedimentos_filters &= Q(pedimento_app=filters['pedimento_app'])
if filters.get('regimen'):
pedimentos_filters &= Q(regimen=filters['regimen'])
if filters.get('tipo_operacion'):
pedimentos_filters &= Q(tipo_operacion_id=filters['tipo_operacion'])
# Consulta asíncrona de los modelos
pedimentos = Pedimento.objects.filter(pedimentos_filters)
filename = filters.get('filename')
if filename:
filename = f"{filename}.csv" if not filename.endswith('.csv') else filename
else:
filename = f"report_{report.id}_{timezone.now().strftime('%Y%m%d%H%M%S')}.csv"
file_path = os.path.join(settings.MEDIA_ROOT, 'reports', filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
headers = [
'aduana', 'patente', 'regimen', 'pedimento', 'pedimento_app', 'clave_pedimento',
'tipo_operacion_id', 'contribuyente_id', 'tipo_documento', 'numero_documento', 'estado', 'acuse_estado'
]
writer.writerow(headers)
for ped in pedimentos:
for cove in Cove.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'COVE', cove.numero_cove, cove.cove_descargado, cove.acuse_cove_descargado
])
for edoc in EDocument.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'EDOC', edoc.numero_edocument, edoc.edocument_descargado, edoc.acuse_descargado
])
for partida in Partida.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'PARTIDA', partida.numero_partida, partida.descargado, ''
])
# Guardar el archivo en el modelo
with open(file_path, 'rb') as f:
report.file.save(filename, ContentFile(f.read()), save=True)
report.status = 'ready'
report.finished_at = timezone.now()
report.save(update_fields=['status', 'file', 'finished_at'])
except Exception as e:
report.status = 'error'
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])

View File

@@ -1,12 +1,14 @@
from django.urls import path, include from django.urls import path, include
from .views import ExportModelView, dashboard_summary from .views import ExportModelView, dashboard_summary
# from .views_stats import documentos_por_fecha # from .views_stats import documentos_por_fecha
# from .views_table import table_summary from .views_table import table_summary, report_document_status, report_document_list, report_document_download
urlpatterns = [ urlpatterns = [
path('exportmodel/', ExportModelView.as_view(), name='export-model'), path('exportmodel/', ExportModelView.as_view(), name='export-model'),
path('dashboard/summary/', dashboard_summary, name='dashboard-summary'), path('dashboard/summary/', dashboard_summary, name='dashboard-summary'),
#path('documentos-por-fecha/', documentos_por_fecha, name='documentos-por-fecha'), #path('documentos-por-fecha/', documentos_por_fecha, name='documentos-por-fecha'),
#path('table-summary/', table_summary, name='table-summary'), path('table-summary/', table_summary, name='table-summary'),
path('report-document-status/<int:report_id>/', report_document_status, name='report_document_status'),
path('report-document-list/', report_document_list, name='report_document_list'),
path('report-document-download/<int:report_id>/', report_document_download, name='report_document_download'),
] ]

View File

@@ -1,27 +1,19 @@
from api.reports.models import ReportDocument
from api.reports.tasks.report_document import generate_report_document
from django.http import FileResponse
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django.db.models import Value, CharField, Q, Exists, OuterRef, Subquery
from django.db.models.functions import Cast
from datetime import datetime, timedelta
from api.customs.models import Pedimento, Cove, EDocument, Partida
class CustomPagination(PageNumberPagination):
page_size = 50
page_size_query_param = 'page_size'
max_page_size = 1000
@api_view(['GET']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def table_summary(request): def table_summary(request):
""" """
Endpoint que devuelve un resumen tabulado de pedimentos y sus documentos asociados. Solo dispara la tarea asíncrona para generar el reporte CSV. No consulta ni procesa datos.
""" """
org_id = request.query_params.get('organizacion_id') org_id = request.query_params.get('organizacion_id')
if not org_id: if not org_id:
return Response({"error": "organizacion_id es requerido"}, status=400) return Response({"error": "organizacion_id es requerido"}, status=400)
# Obtener filtros de query params # Obtener filtros de query params
tipo_documento = request.query_params.get('tipo_documento') tipo_documento = request.query_params.get('tipo_documento')
rfc = request.query_params.get('contribuyente__rfc') rfc = request.query_params.get('contribuyente__rfc')
@@ -33,137 +25,93 @@ def table_summary(request):
pedimento_app = request.query_params.get('pedimento_app') pedimento_app = request.query_params.get('pedimento_app')
regimen = request.query_params.get('regimen') regimen = request.query_params.get('regimen')
tipo_operacion = request.query_params.get('tipo_operacion') tipo_operacion = request.query_params.get('tipo_operacion')
filename_param = request.query_params.get('filename')
# Si no se proporcionan fechas, establecer un rango por defecto de los últimos 30 días if filename_param:
if not fecha_pago_gte and not fecha_pago_lte: filename = filename_param
fecha_pago_lte = datetime.now().date() else:
fecha_pago_gte = fecha_pago_lte - timedelta(days=30) filename = None
# Si no se proporcionan fechas, no las incluyas en los filtros
# Construir filtros base para pedimentos if not fecha_pago_gte:
pedimentos_filters = Q(organizacion_id=org_id) fecha_pago_gte_str = None
elif isinstance(fecha_pago_gte, str):
# Añadir filtros de fecha siempre para limitar el conjunto de datos fecha_pago_gte_str = fecha_pago_gte
pedimentos_filters &= Q(fecha_pago__gte=fecha_pago_gte) else:
pedimentos_filters &= Q(fecha_pago__lte=fecha_pago_lte) fecha_pago_gte_str = fecha_pago_gte.isoformat()
if not fecha_pago_lte:
if rfc: fecha_pago_lte_str = None
pedimentos_filters &= Q(contribuyente__rfc=rfc) elif isinstance(fecha_pago_lte, str):
if patente: fecha_pago_lte_str = fecha_pago_lte
pedimentos_filters &= Q(patente=patente) else:
if aduana: fecha_pago_lte_str = fecha_pago_lte.isoformat()
pedimentos_filters &= Q(aduana=aduana) filtros = {
if pedimento: "organizacion_id": org_id,
pedimentos_filters &= Q(pedimento=pedimento) "tipo_documento": tipo_documento,
if pedimento_app: "contribuyente__rfc": rfc,
pedimentos_filters &= Q(pedimento_app=pedimento_app) "fecha_pago__gte": fecha_pago_gte_str,
if regimen: "fecha_pago__lte": fecha_pago_lte_str,
pedimentos_filters &= Q(regimen=regimen) "patente": patente,
if tipo_operacion: "aduana": aduana,
pedimentos_filters &= Q(tipo_operacion_id=tipo_operacion) "pedimento": pedimento,
"pedimento_app": pedimento_app,
# Query base desde pedimentos con todas las subconsultas necesarias "regimen": regimen,
resultado = Pedimento.objects.filter(pedimentos_filters).values( "tipo_operacion": tipo_operacion,
'aduana', "filename": filename
'patente', }
'regimen', report = ReportDocument.objects.create(
'pedimento', user=request.user,
'pedimento_app', filters=filtros,
'clave_pedimento', status='pending'
'tipo_operacion_id',
'contribuyente_id'
) )
generate_report_document.delay(report.id)
return Response({
"report_id": report.id,
"status": report.status,
"created_at": report.created_at,
"download_url": report.file.url if report.file else None
}, status=202)
# Generar queries según el tipo de documento solicitado @api_view(['GET'])
queries = [] @permission_classes([IsAuthenticated])
def report_document_status(request, report_id):
if not tipo_documento or tipo_documento == 'ACUSE COVE': try:
coves_acuse = resultado.annotate( report = ReportDocument.objects.get(id=report_id, user=request.user)
identificador=Cast(Subquery( data = {
Cove.objects.filter(pedimento_id=OuterRef('id')).values('numero_cove')[:1] "report_id": report.id,
), CharField()), "status": report.status,
documento=Value('ACUSE COVE', CharField()), "created_at": report.created_at,
estado=Cast(Subquery( "finished_at": report.finished_at,
Cove.objects.filter(pedimento_id=OuterRef('id')).values('acuse_cove_descargado')[:1] "error_message": report.error_message,
), CharField()) "download_url": report.file.url if report.file else None
).filter(identificador__isnull=False)
queries.append(coves_acuse)
if not tipo_documento or tipo_documento == 'COVE':
coves = resultado.annotate(
identificador=Cast(Subquery(
Cove.objects.filter(pedimento_id=OuterRef('id')).values('numero_cove')[:1]
), CharField()),
documento=Value('COVE', CharField()),
estado=Cast(Subquery(
Cove.objects.filter(pedimento_id=OuterRef('id')).values('cove_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(coves)
if not tipo_documento or tipo_documento == 'ACUSE EDOC':
edocs_acuse = resultado.annotate(
identificador=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('numero_edocument')[:1]
), CharField()),
documento=Value('ACUSE EDOC', CharField()),
estado=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('acuse_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(edocs_acuse)
if not tipo_documento or tipo_documento == 'EDOC':
edocs = resultado.annotate(
identificador=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('numero_edocument')[:1]
), CharField()),
documento=Value('EDOC', CharField()),
estado=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('edocument_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(edocs)
if not tipo_documento or tipo_documento == 'PARTIDA':
partidas = resultado.annotate(
identificador=Cast(Subquery(
Partida.objects.filter(pedimento_id=OuterRef('id')).values('numero_partida')[:1]
), CharField()),
documento=Value('PARTIDA', CharField()),
estado=Cast(Subquery(
Partida.objects.filter(pedimento_id=OuterRef('id')).values('descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(partidas)
# Unir los resultados usando UNION ALL para mejor rendimiento
if not queries:
return Response([])
resultado_final = queries[0]
for query in queries[1:]:
resultado_final = resultado_final.union(query, all=True)
# Aplicar paginación
paginator = CustomPagination()
page = paginator.paginate_queryset(
resultado_final.order_by('pedimento', 'documento'),
request
)
return paginator.get_paginated_response({
"results": page,
"filtros_aplicados": {
"organizacion_id": org_id,
"tipo_documento": tipo_documento,
"contribuyente__rfc": rfc,
"fecha_pago__gte": fecha_pago_gte,
"fecha_pago__lte": fecha_pago_lte,
"patente": patente,
"aduana": aduana,
"pedimento": pedimento,
"pedimento_app": pedimento_app,
"regimen": regimen,
"tipo_operacion": tipo_operacion
} }
}) return Response(data)
except ReportDocument.DoesNotExist:
return Response({"error": "Reporte no encontrado"}, status=404)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def report_document_list(request):
reports = ReportDocument.objects.filter(user=request.user).order_by('-created_at')
data = [
{
"report_id": r.id,
"status": r.status,
"created_at": r.created_at,
"finished_at": r.finished_at,
"error_message": r.error_message,
"download_url": r.file.url if r.file else None
}
for r in reports
]
return Response(data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def report_document_download(request, report_id):
try:
report = ReportDocument.objects.get(id=report_id, user=request.user)
if not report.file:
return Response({"error": "El archivo aún no está disponible"}, status=404)
response = FileResponse(report.file.open('rb'), as_attachment=True, filename=report.file.name)
return response
except ReportDocument.DoesNotExist:
return Response({"error": "Reporte no encontrado"}, status=404)

14
api/tasks/filters.py Normal file
View File

@@ -0,0 +1,14 @@
from django_filters import rest_framework as filters
from .models import Task
class TaskFilter(filters.FilterSet):
servicio = filters.NumberFilter(field_name='servicio__id')
pedimento_app = filters.CharFilter(field_name='pedimento__pedimento_app')
pedimento = filters.UUIDFilter(field_name='pedimento__id')
timestamp_gte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='gte')
timestamp_lte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='lte')
status = filters.CharFilter(field_name='status')
class Meta:
model = Task
fields = ['servicio', 'pedimento_app', 'pedimento', 'timestamp_gte', 'timestamp_lte', 'status']

View File

@@ -9,3 +9,4 @@ class Task(models.Model):
message = models.TextField() message = models.TextField()
status = models.CharField(max_length=50) status = models.CharField(max_length=50)
servicio = models.ForeignKey('customs.Servicio', on_delete=models.CASCADE, null=True, blank=True) servicio = models.ForeignKey('customs.Servicio', on_delete=models.CASCADE, null=True, blank=True)

View File

@@ -2,6 +2,8 @@ from rest_framework import serializers
from .models import Task from .models import Task
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
pedimento_app = serializers.CharField(source='pedimento.pedimento_app', read_only=True)
class Meta: class Meta:
model = Task model = Task
fields = '__all__' fields = '__all__'

View File

@@ -1,9 +1,12 @@
from django.shortcuts import render from django.shortcuts import render
from rest_framework import viewsets from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.pagination import PageNumberPagination
from api.logger.mixins import LoggingMixin from api.logger.mixins import LoggingMixin
from .models import Task from .models import Task
from .serializers import TaskSerializer from .serializers import TaskSerializer
from .filters import TaskFilter
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
# Create your views here. # Create your views here.
@@ -14,9 +17,19 @@ from core.permissions import (
IsSuperUser IsSuperUser
) )
class TaskPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class TaskViewSet(LoggingMixin,viewsets.ModelViewSet): class TaskViewSet(LoggingMixin,viewsets.ModelViewSet):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
queryset = Task.objects.all() queryset = Task.objects.select_related('pedimento', 'servicio').all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_class = TaskFilter
pagination_class = TaskPagination
ordering_fields = ['timestamp']
ordering = ['-timestamp'] # ordenamiento por defecto, más reciente primero
my_tags = ['tasks'] my_tags = ['tasks']

View File

@@ -1,61 +0,0 @@
#!/bin/bash
echo "Iniciando limpieza completa de archivos media huérfanos..."
# Función para ejecutar limpieza
cleanup_batch() {
echo "Ejecutando limpieza de lote..."
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --verbose" 2>/dev/null
return $?
}
# Contador de iteraciones
iteration=1
total_cleaned=0
while true; do
echo "=== Iteración $iteration ==="
# Ejecutar dry-run para ver si hay archivos huérfanos
result=$(docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --dry-run" 2>/dev/null)
# Extraer número de archivos huérfanos
orphaned_count=$(echo "$result" | grep -o "Archivos huérfanos encontrados: [0-9]*" | grep -o "[0-9]*")
if [ -z "$orphaned_count" ] || [ "$orphaned_count" -eq 0 ]; then
echo "✅ No se encontraron más archivos huérfanos."
echo "🎉 Limpieza completa finalizada después de $iteration iteraciones."
echo "📊 Total estimado de archivos eliminados: $total_cleaned"
break
fi
echo "📁 Encontrados $orphaned_count archivos huérfanos en esta iteración"
# Preguntar confirmación en la primera iteración
if [ $iteration -eq 1 ]; then
echo "¿Continuar con la limpieza automática? (s/N):"
read -r response
if [[ ! "$response" =~ ^[sS]([iI]|í)?$ ]]; then
echo "❌ Limpieza cancelada por el usuario."
exit 0
fi
fi
# Ejecutar limpieza
cleanup_result=$(docker exec -it EFC_backend_dev bash -c "cd /app && echo 's' | python manage.py cleanup_media_fast --quick-scan --verbose" 2>/dev/null)
# Extraer archivos eliminados
deleted_count=$(echo "$cleanup_result" | grep -o "Eliminados [0-9]* archivos" | grep -o "[0-9]*")
if [ -n "$deleted_count" ]; then
total_cleaned=$((total_cleaned + deleted_count))
echo "✅ Eliminados $deleted_count archivos en esta iteración"
fi
iteration=$((iteration + 1))
# Pausa pequeña para evitar sobrecarga
sleep 2
done
echo "🏁 Proceso completado exitosamente."

View File

@@ -1,50 +1,5 @@
# Celery Beat Schedule
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
# Ejecutar pedimento completo de 5:00 a 22:00 (cada hora)
'creacion-servicio-pedimento-completo': {
'task': 'api.customs.tasks.internal_services.crear_todos_los_servicios',
'schedule': crontab(minute=0, hour='5-22'),
},
# Ejecutar pedimento completo de 5:00 a 22:00 (cada hora)
'ejecutar-pedimentos-completos-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_pedimento_completo',
'schedule': crontab(minute=0, hour='5-22'),
},
# Ejecutar partidas de 5:00 a 22:00 (cada hora)
'ejecutar-partidas-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_partidas_pedimento',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar coves de 5:00 a 22:00 (cada hora)
'ejecutar-coves-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_coves',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar remesas de 5:00 a 22:00 (cada hora)
'ejecutar-remesas-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_remesas',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar acuse coves de 5:00 a 22:00 (cada hora)
'ejecutar-acuse-coves-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_acuseCoves',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar acuse de 5:00 a 22:00 (cada hora)
'ejecutar-acuse-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_acuse',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar edocs solo de 23:00 a 4:59 (cada hora en ese rango)
'ejecutar-edocs-noche': {
'task': 'api.customs.tasks.microservice.ejecutar_edocs',
'schedule': crontab(minute=42, hour='23,0,1,2,3,4'),
},
}
""" """
Django settings for config project. Django settings for config project.
@@ -70,6 +25,14 @@ smtplib.SMTP_SSL.default_context = ssl._create_unverified_context
from dotenv import load_dotenv from dotenv import load_dotenv
import re import re
# Celery Beat Schedule
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
}
# Cargar variables de entorno desde un archivo .env # Cargar variables de entorno desde un archivo .env
load_dotenv() load_dotenv()
@@ -305,7 +268,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Ojinaga' # Zona horaria de Cd. Juárez, Chihuahua TIME_ZONE = 'America/Mexico_City' # Zona horaria de Cd. Juárez, Chihuahua
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True

View File

@@ -0,0 +1,577 @@
type: collection.insomnia.rest/5.0
name: EFC
meta:
id: wrk_71a7fc7f009b45338f10d5aee912e90c
created: 1759368209169
modified: 1759415563572
description: ""
collection:
- url: "{{ _.API_REST
}}customs/coves/?pedimento=7e199074-db14-4431-8ded-2028dcd7ba23&page=1&pa\
ge_size=10"
name: Coves
meta:
id: req_a90087be376f42eb846e9c0a080c1e0d
created: 1759368209176
modified: 1759368596683
isPrivate: false
description: ""
sortKey: -1759368209176
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST
}}customs/edocuments/?pedimento=7e199074-db14-4431-8ded-2028dcd7ba23&page\
=1&page_size=10"
name: EDocs
meta:
id: req_40ed26a5256d49f0bfe05716237131dd
created: 1759368603480
modified: 1759368614527
isPrivate: false
description: ""
sortKey: -1759285093434.5
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST
}}customs/procesamientopedimentos/?pedimento=7e199074-db14-4431-8ded-2028\
dcd7ba23&page=1&page_size=10"
name: Procesamientos del Pedimento
meta:
id: req_bba2280f3e894ada879b734a24c78414
created: 1759368715006
modified: 1759368736956
isPrivate: false
description: ""
sortKey: -1759243535563.75
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: http://192.168.1.79:8000/api/v1/customs/partidas/?pedimento=5ed0a19a-8a42-4ab4-8627-1a614be7aee3
name: New Request
meta:
id: req_680b0285689f46d99857ca2765a36e30
created: 1759457224319
modified: 1759717890407
isPrivate: false
description: ""
sortKey: -1759457224319
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_ea89b2c93a934f72ab8183aa7bcddfb3
- id: pair_be239987c9a648debc7c318ab3d9edd4
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}customs/partidas/771281/"
name: Partida
meta:
id: req_c911fa79111e45fc9d592a4dff98889a
created: 1759521825542
modified: 1759522450563
isPrivate: false
description: ""
sortKey: -1759326651305.25
method: PUT
body:
mimeType: application/json
text: "{
\t\"id\": 771281,
\t\"numero_partida\": 1,
\t\"descargado\": true,
\t\"pedimento\": \"0731973d-3929-43f5-805b-a8680fbadd30\",
\t\"organizacion\": \"4fea91c7-4a1d-40b3-a433-f0122b5ea43e\"
}
\t"
headers:
- name: Content-Type
value: application/json
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}vucem/vucem/?importador={{pedimento.contribuyente_id}}"
name: Vucem
meta:
id: req_4b049e9b11844f158762682b9118e572
created: 1759523330397
modified: 1759523405870
isPrivate: false
description: ""
sortKey: -1759264314499.125
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/dashboard/summary/"
name: dashboard_summary
meta:
id: req_0d68721909b54ae983f31f91c8acdd4c
created: 1759623976466
modified: 1759710185608
isPrivate: false
description: ""
sortKey: -1759253925031.4375
method: GET
parameters:
- id: pair_9805d9f69b664b9d903fd142a6a940fb
name: pedimento_app
value: 20-16-3910-1000
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/documentos-por-fecha/"
name: Documentos por fecha
meta:
id: req_733c9cae6cbe4ccca8ccebe2efa5c3ba
created: 1759628575231
modified: 1759634973470
isPrivate: false
description: ""
sortKey: -1759248730297.5938
method: GET
parameters:
- id: pair_ec4119b532f94b579dfc54ddc2ed8979
name: periodo
value: semana
description: ""
disabled: false
- id: pair_d10e813bf42a45038907b3658df2dbeb
name: start_date
value: 2010-01-01
description: ""
disabled: false
- id: pair_3ab5417081e34862b9d3cc80b7125432
name: end_date
value: 2026-01-01
description: ""
disabled: false
- id: pair_74805c59d3704db3893d424e6580110a
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_fb9f6ce9c82f4db8834b6443f3fcea44
name: tipo_documento
value: "1"
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/table-summary/"
name: Summary Table
meta:
id: req_259a849b4a8e4a34b85f53644e7d5939
created: 1759634983441
modified: 1761103843982
isPrivate: false
description: ""
sortKey: -1759251327664.5156
method: GET
parameters:
- id: pair_320fbf127da544acae95eb8524daa1db
name: csv_async
value: "1"
description: ""
disabled: false
- id: pair_99f1f48e9db0439a950c706d5767657c
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_8900c16ca9894ac8b559284dde8feca8
name: page
value: "1"
description: ""
disabled: true
- id: pair_9d3852c3e1bb4dd6b6ea838df070a65d
name: page_size
value: "100"
description: ""
disabled: true
- id: pair_7ea23e87e0874588bf4d17041eaaa3c6
name: fecha_pago__lte
value: 2024-01-01
description: ""
disabled: true
- id: pair_a3c85e2563d24ec0ba772a2f6e27804a
name: ultimos
value: "1"
description: ""
disabled: true
- id: pair_e013d47373884f99b3f4def8592b2f01
name: filename
value: test_kevin
description: ""
disabled: true
- id: pair_9ff11357a7044e5b8f40bb9bb33e6e12
name: contribuyente__rfc
value: TEC1406248Q2
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}customs/pedimentos/"
name: Pedimento
meta:
id: req_9a679fe2a36b42949200a0cc93e7cc76
created: 1759717886641
modified: 1759718950361
isPrivate: false
description: ""
sortKey: -1759717886641
method: POST
body:
mimeType: application/json
text: |-
{
"pedimento": "946",
"patente": "3910",
"aduana": "160",
"regimen": "ITE",
"clave_pedimento": "IN",
"fecha_inicio": "2025-08-07",
"fecha_fin": "2025-08-07",
"fecha_pago": "2020-03-11",
"alerta": true,
"agente_aduanal": "AAC020726T77",
"curp_apoderado": "DAXS571117HNEVXR09",
"importe_total": 0.0,
"saldo_disponible": 0.0,
"importe_pedimento": 0.0,
"existe_expediente": true,
"remesas": true,
"numero_partidas": 0,
"numero_operacion": "string",
"tipo_operacion": 1,
"contribuyente": "TEC1406248Q2",
"organizacion":"9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4"
}
headers:
- name: Content-Type
value: application/json
- name: User-Agent
value: insomnia/11.6.1
id: pair_ed0ede3d111d469ea71b9c09f99134df
- id: pair_1db698171c6a4534a683351e27b61d9f
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: api.efc-aduanasoft.com/api/v1/customs/auditor/crear-partidas/pedimento/
name: Auditor Partidas
meta:
id: req_82ccc9ebce664984826063dd2e86bbc1
created: 1759720753371
modified: 1759721186316
isPrivate: false
description: ""
sortKey: -1759619270481.5
method: POST
body:
mimeType: application/json
text: |-
{
"pedimento_id": "54ed7616-aea9-46ca-887e-3185feb35d09"
}
headers:
- name: Content-Type
value: application/json
id: pair_5afd216181cd4050a683cc050a15f8af
- name: User-Agent
value: insomnia/11.6.1
id: pair_ed0ede3d111d469ea71b9c09f99134df
- id: pair_1db698171c6a4534a683351e27b61d9f
name: Authorization
value: Token 27ad1a1a7a1e7a64e7f452e89265b54b5b3ef4c2
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/report-document-list/"
name: Reports List
meta:
id: req_7a951a204e8b454e81b25c059ad8ff18
created: 1761091292956
modified: 1761094591411
isPrivate: false
description: ""
sortKey: -1759250028981.0547
method: GET
parameters:
- id: pair_320fbf127da544acae95eb8524daa1db
name: csv_async
value: "1"
description: ""
disabled: false
- id: pair_99f1f48e9db0439a950c706d5767657c
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_8900c16ca9894ac8b559284dde8feca8
name: page
value: "1"
description: ""
disabled: true
- id: pair_9d3852c3e1bb4dd6b6ea838df070a65d
name: page_size
value: "100"
description: ""
disabled: true
- id: pair_7ea23e87e0874588bf4d17041eaaa3c6
name: fecha_pago__lte
value: 2024-01-01
description: ""
disabled: false
- id: pair_a3c85e2563d24ec0ba772a2f6e27804a
name: ultimos
value: "1"
description: ""
disabled: true
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/report-document-download/11/"
name: Download Report
meta:
id: req_11dc9145994b43798c69ee3ecebe8ba8
created: 1761092998357
modified: 1761093919398
isPrivate: false
description: ""
sortKey: -1759249379639.3242
method: GET
parameters:
- id: pair_320fbf127da544acae95eb8524daa1db
name: csv_async
value: "1"
description: ""
disabled: false
- id: pair_99f1f48e9db0439a950c706d5767657c
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_8900c16ca9894ac8b559284dde8feca8
name: page
value: "1"
description: ""
disabled: true
- id: pair_9d3852c3e1bb4dd6b6ea838df070a65d
name: page_size
value: "100"
description: ""
disabled: true
- id: pair_7ea23e87e0874588bf4d17041eaaa3c6
name: fecha_pago__lte
value: 2024-01-01
description: ""
disabled: false
- id: pair_a3c85e2563d24ec0ba772a2f6e27804a
name: ultimos
value: "1"
description: ""
disabled: false
- id: pair_ab1873fdc82f4b8d898a5f0004cae1fe
name: "rfc "
value: MTK861014317
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
cookieJar:
name: Default Jar
meta:
id: jar_7ef8f609d3e9f73da7feb30ab71eb8baaaf31c81
created: 1759368209171
modified: 1759368209171
environments:
name: Base Environment
meta:
id: env_7ef8f609d3e9f73da7feb30ab71eb8baaaf31c81
created: 1759368209170
modified: 1759719172699
isPrivate: false
data:
API_REST: http://192.168.1.79:8000/api/v1/
Token: 27ad1a1a7a1e7a64e7f452e89265b54b5b3ef4c2

View File

@@ -0,0 +1,52 @@
# Comando de Microservicios
Este comando permite disparar tareas de procesamiento masivo por organización o por pedimento en el sistema EFC.
## Uso básico
```bash
python manage.py microservicios [--organizacion_id=<id>] [--procesamiento=<tipo>] [--todos]
```
## Opciones
- `--organizacion_id <id>`: Ejecuta los procesamientos solo para la organización indicada. Si se omite, se procesan todas las organizaciones.
- `--procesamiento <tipo>`: Ejecuta solo el tipo de procesamiento indicado para la organización. Si se omite, se ejecutan todos los procesamientos.
- `--todos`: Ejecuta todos los procesos para todas las organizaciones.
## Tipos de procesamiento disponibles
- `coves`: Procesa COVEs por organización/pedimento
- `edocs`: Procesa E-Documents por organización/pedimento
- `acuses`: Procesa acuses por organización/pedimento
- `acuse_coves`: Procesa acuse de COVEs por organización/pedimento
- `partidas`: Procesa partidas por organización/pedimento
- `pedimentos_completos`: Procesa pedimentos completos por organización/pedimento
- `remesas`: Procesa remesas por organización/pedimento
## Ejemplos
### Procesar todos los tipos para todas las organizaciones
```bash
python manage.py microservicios --todos
```
### Procesar solo COVEs para una organización
```bash
python manage.py microservicios --organizacion_id=ID_ORG --procesamiento=coves
```
### Procesar todos los tipos para una organización
```bash
python manage.py microservicios --organizacion_id=ID_ORG
```
### Procesar todos los tipos para un pedimento específico
```bash
python manage.py microservicios --pedimento_id=ID_PEDIMENTO
```
## Notas
- El comando dispara las tareas de Celery de forma asíncrona.
- Revisa los logs y el estado de las tareas en el panel de administración de Celery o en los logs del sistema.
- Si agregas nuevos tipos de procesamiento, actualiza la función `ejecutar_procesamiento_por_organizacion` en `microservice_v2.py`.