fix/de los tickets T2026-05-027, T2025-09-004 y T2025-09-056
This commit is contained in:
@@ -23,6 +23,7 @@ from core.permissions import (
|
||||
get_org_context,
|
||||
require_permission,
|
||||
user_has_permission,
|
||||
user_has_role,
|
||||
is_internal_service_request,
|
||||
)
|
||||
from api.customs.models import (
|
||||
@@ -33,6 +34,7 @@ from api.customs.models import (
|
||||
Cove,
|
||||
Importador,
|
||||
Partida,
|
||||
EstadoDescarga,
|
||||
)
|
||||
from api.customs.serializers import (
|
||||
PedimentoSerializer,
|
||||
@@ -2338,9 +2340,19 @@ class PartidaViewSet(viewsets.ModelViewSet):
|
||||
if not org:
|
||||
return Partida.objects.none()
|
||||
qs = Partida.objects.filter(pedimento__organizacion=org)
|
||||
# Misma precedencia que los mixins de filtrado: superuser y roles
|
||||
# operativos ven todo lo de su org; is_importador no los degrada.
|
||||
if (
|
||||
user.is_superuser or
|
||||
user_has_role(user, 'admin') or
|
||||
user_has_role(user, 'developer') or
|
||||
user_has_role(user, 'Agente Aduanal') or
|
||||
user_has_role(user, 'user')
|
||||
):
|
||||
return qs
|
||||
if user.is_importador:
|
||||
qs = qs.filter(pedimento__contribuyente__in=user.rfc.all())
|
||||
return qs
|
||||
return qs.filter(pedimento__contribuyente__in=user.rfc.all())
|
||||
return Partida.objects.none()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if is_internal_service_request(self.request):
|
||||
@@ -2456,12 +2468,20 @@ class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizaci
|
||||
org = get_org_context(user)
|
||||
if not org:
|
||||
return ProcesamientoPedimento.objects.none()
|
||||
qs = ProcesamientoPedimento.objects.filter(organizacion=org)
|
||||
# Misma precedencia que los mixins de filtrado: superuser y roles
|
||||
# operativos ven todo lo de su org; is_importador no los degrada.
|
||||
if (
|
||||
user.is_superuser or
|
||||
user_has_role(user, 'admin') or
|
||||
user_has_role(user, 'developer') or
|
||||
user_has_role(user, 'Agente Aduanal') or
|
||||
user_has_role(user, 'user')
|
||||
):
|
||||
return qs
|
||||
if user.is_importador:
|
||||
return ProcesamientoPedimento.objects.filter(
|
||||
organizacion=org,
|
||||
pedimento__contribuyente__in=user.rfc.all()
|
||||
)
|
||||
return ProcesamientoPedimento.objects.filter(organizacion=org)
|
||||
return qs.filter(pedimento__contribuyente__in=user.rfc.all())
|
||||
return ProcesamientoPedimento.objects.none()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if is_internal_service_request(self.request):
|
||||
@@ -2485,6 +2505,53 @@ class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizaci
|
||||
|
||||
my_tags = ['Procesamientos_Pedimentos']
|
||||
|
||||
def _crear_documento_error_vu(registro, numero, doc_type_error_id, mensaje, file_prefix='error_vu'):
|
||||
"""
|
||||
Crea un Document de error VU (tipos 20/22/24/26) para dejar evidencia en la
|
||||
pestaña Errores VU cuando se detecta una inconsistencia de descarga.
|
||||
`registro` debe tener pedimento y organizacion. El nombre del archivo incluye
|
||||
el número del registro para que el frontend lo asocie (archivo__icontains).
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger('api.customs.views')
|
||||
|
||||
doc_type_error = DocumentType.objects.filter(id=doc_type_error_id).first()
|
||||
if not doc_type_error:
|
||||
return
|
||||
|
||||
error_content = mensaje.encode('utf-8')
|
||||
tmp_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode='wb', suffix='.txt', delete=False) as f:
|
||||
f.write(error_content)
|
||||
tmp_path = f.name
|
||||
|
||||
pedimento_app = getattr(registro.pedimento, 'pedimento_app', str(registro.pedimento.pedimento))
|
||||
file_name = f"{file_prefix}_{numero}.txt"
|
||||
|
||||
saved_path = storage_service.save_document_from_path(
|
||||
file_path=tmp_path,
|
||||
file_name=file_name,
|
||||
organizacion_id=registro.organizacion_id,
|
||||
pedimento_app=pedimento_app
|
||||
)
|
||||
|
||||
if saved_path:
|
||||
Document.objects.create(
|
||||
organizacion=registro.organizacion,
|
||||
pedimento=registro.pedimento,
|
||||
archivo=saved_path,
|
||||
document_type=doc_type_error,
|
||||
extension='TXT',
|
||||
size=len(error_content),
|
||||
fuente=None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creando documento de error VU para {numero}: {e}")
|
||||
finally:
|
||||
if tmp_path and os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
|
||||
class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||
"""
|
||||
ViewSet for EDocument model.
|
||||
@@ -2492,7 +2559,18 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
||||
serializer_class = EDocumentSerializer
|
||||
pagination_class = CustomPagination
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['pedimento', 'numero_edocument', 'organizacion']
|
||||
filterset_fields = {
|
||||
'pedimento': ['exact'],
|
||||
'numero_edocument': ['exact', 'icontains'],
|
||||
'organizacion': ['exact'],
|
||||
'clave': ['exact', 'icontains'],
|
||||
'descripcion': ['icontains'],
|
||||
'edocument_descargado': ['exact'],
|
||||
'acuse_descargado': ['exact'],
|
||||
'edocument_estado': ['exact'],
|
||||
'acuse_estado': ['exact'],
|
||||
'created_at': ['gte', 'lte'],
|
||||
}
|
||||
search_fields = ['numero_edocument', 'descripcion', 'organizacion']
|
||||
ordering_fields = ['created_at', 'updated_at', 'numero_edocument']
|
||||
ordering = ['-created_at']
|
||||
@@ -2510,6 +2588,7 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
||||
'destroy': 'edocuments.delete',
|
||||
'bulk_delete_edocs_vu': 'edocuments.delete',
|
||||
'reset_acuse': 'edocuments.edit',
|
||||
'reset_edocument': 'edocuments.edit',
|
||||
}
|
||||
codename = perms.get(self.action, 'edocuments.view')
|
||||
return [IsAuthenticated(), require_permission(codename)()]
|
||||
@@ -2539,28 +2618,30 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
||||
@action(detail=True, methods=['post'], url_path='reset-acuse')
|
||||
def reset_acuse(self, request, pk=None):
|
||||
"""
|
||||
Detecta inconsistencia cuando acuse_descargado=True pero no existe el documento
|
||||
de acuse (tipo 4). Crea un registro de error tipo 26 para Errores VU y
|
||||
restablece acuse_descargado=False para permitir reintentar.
|
||||
Detecta inconsistencia cuando el acuse está marcado como descargado pero el
|
||||
documento de acuse (tipo 4) no existe en BD o el archivo falta en storage.
|
||||
Crea un registro de error tipo 26 para Errores VU y restablece
|
||||
acuse_estado='pendiente' con contador de intentos en 0 — única vía que
|
||||
re-habilita el reintento automático (T2026-05-027).
|
||||
"""
|
||||
from api.record.models import Document, DocumentType
|
||||
import logging
|
||||
logger = logging.getLogger('api.customs.views')
|
||||
|
||||
edoc = self.get_object()
|
||||
|
||||
if not edoc.acuse_descargado:
|
||||
if edoc.acuse_estado != EstadoDescarga.DESCARGADO:
|
||||
return Response(
|
||||
{"error": "El acuse no está marcado como descargado"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Verificar si el acuse PDF (tipo 4 = Pedimento Acuse) existe realmente
|
||||
acuse_disponible = Document.objects.filter(
|
||||
# Verificar el acuse (tipo 4 = Pedimento Acuse) en BD y físicamente en storage
|
||||
acuse_docs = Document.objects.filter(
|
||||
pedimento=edoc.pedimento,
|
||||
archivo__icontains=edoc.numero_edocument,
|
||||
document_type_id=4
|
||||
).exists()
|
||||
)
|
||||
acuse_disponible = any(
|
||||
doc.size and storage_service.file_exists(doc.archivo.name)
|
||||
for doc in acuse_docs
|
||||
)
|
||||
|
||||
if acuse_disponible:
|
||||
return Response(
|
||||
@@ -2568,51 +2649,74 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# Inconsistencia confirmada: crear documento de error tipo 26 para Errores VU
|
||||
doc_type_error = DocumentType.objects.filter(id=26).first()
|
||||
if doc_type_error:
|
||||
error_content = (
|
||||
# Inconsistencia confirmada: dejar evidencia en Errores VU (tipo 26)
|
||||
_crear_documento_error_vu(
|
||||
registro=edoc,
|
||||
numero=edoc.numero_edocument,
|
||||
doc_type_error_id=26,
|
||||
mensaje=(
|
||||
f"Inconsistencia detectada: el acuse del EDocument {edoc.numero_edocument} "
|
||||
f"fue marcado como descargado pero el documento no se encuentra disponible. "
|
||||
f"El estado fue restablecido para permitir reprocesamiento."
|
||||
).encode('utf-8')
|
||||
),
|
||||
file_prefix='error_acuse',
|
||||
)
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='wb', suffix='.txt', delete=False
|
||||
) as f:
|
||||
f.write(error_content)
|
||||
tmp_path = f.name
|
||||
edoc.acuse_estado = EstadoDescarga.PENDIENTE
|
||||
edoc.acuse_intentos = 0
|
||||
edoc.ultimo_error = None
|
||||
edoc.save()
|
||||
|
||||
pedimento_app = getattr(edoc.pedimento, 'pedimento_app', str(edoc.pedimento.pedimento))
|
||||
file_name = f"error_acuse_{edoc.numero_edocument}.txt"
|
||||
serializer = self.get_serializer(edoc)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
saved_path = storage_service.save_document_from_path(
|
||||
file_path=tmp_path,
|
||||
file_name=file_name,
|
||||
organizacion_id=edoc.organizacion_id,
|
||||
pedimento_app=pedimento_app
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='reset-edocument')
|
||||
def reset_edocument(self, request, pk=None):
|
||||
"""
|
||||
Igual que reset-acuse pero para el documento general del EDocument: si está
|
||||
marcado como descargado sin documento disponible (BD o storage), crea error
|
||||
tipo 22 y restablece edocument_estado='pendiente' con contador en 0.
|
||||
"""
|
||||
edoc = self.get_object()
|
||||
|
||||
if saved_path:
|
||||
Document.objects.create(
|
||||
organizacion=edoc.organizacion,
|
||||
pedimento=edoc.pedimento,
|
||||
archivo=saved_path,
|
||||
document_type=doc_type_error,
|
||||
extension='TXT',
|
||||
size=len(error_content),
|
||||
fuente=None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error creando documento de error para acuse {edoc.numero_edocument}: {e}"
|
||||
)
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
if edoc.edocument_estado != EstadoDescarga.DESCARGADO:
|
||||
return Response(
|
||||
{"error": "El e-documento no está marcado como descargado"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
edoc.acuse_descargado = False
|
||||
# Documentos generales del EDocument: se excluyen acuse (4), requests (21, 25)
|
||||
# y errores (22, 26) del catálogo document_type
|
||||
edoc_docs = Document.objects.filter(
|
||||
pedimento=edoc.pedimento,
|
||||
archivo__icontains=edoc.numero_edocument,
|
||||
).exclude(document_type_id__in=[4, 21, 22, 25, 26])
|
||||
edoc_disponible = any(
|
||||
doc.size and storage_service.file_exists(doc.archivo.name)
|
||||
for doc in edoc_docs
|
||||
)
|
||||
|
||||
if edoc_disponible:
|
||||
return Response(
|
||||
{"status": "El e-documento está disponible correctamente", "edocument_disponible": True},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
_crear_documento_error_vu(
|
||||
registro=edoc,
|
||||
numero=edoc.numero_edocument,
|
||||
doc_type_error_id=22,
|
||||
mensaje=(
|
||||
f"Inconsistencia detectada: el EDocument {edoc.numero_edocument} fue marcado "
|
||||
f"como descargado pero el documento no se encuentra disponible. "
|
||||
f"El estado fue restablecido para permitir reprocesamiento."
|
||||
),
|
||||
file_prefix='error_edocument',
|
||||
)
|
||||
|
||||
edoc.edocument_estado = EstadoDescarga.PENDIENTE
|
||||
edoc.edocument_intentos = 0
|
||||
edoc.ultimo_error = None
|
||||
edoc.save()
|
||||
|
||||
serializer = self.get_serializer(edoc)
|
||||
@@ -2625,7 +2729,16 @@ class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||
serializer_class = CoveSerializer
|
||||
pagination_class = CustomPagination
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['pedimento', 'numero_cove', 'organizacion']
|
||||
filterset_fields = {
|
||||
'pedimento': ['exact'],
|
||||
'numero_cove': ['exact', 'icontains'],
|
||||
'organizacion': ['exact'],
|
||||
'cove_descargado': ['exact'],
|
||||
'acuse_cove_descargado': ['exact'],
|
||||
'cove_estado': ['exact'],
|
||||
'acuse_cove_estado': ['exact'],
|
||||
'created_at': ['gte', 'lte'],
|
||||
}
|
||||
search_fields = ['numero_cove', 'descripcion', 'organizacion']
|
||||
ordering_fields = ['created_at', 'updated_at', 'numero_cove']
|
||||
ordering = ['-created_at']
|
||||
@@ -2642,6 +2755,8 @@ class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||
'partial_update': 'coves.edit',
|
||||
'destroy': 'coves.delete',
|
||||
'bulk_delete_coves_vu': 'coves.delete',
|
||||
'reset_cove': 'coves.edit',
|
||||
'reset_acuse_cove': 'coves.edit',
|
||||
}
|
||||
codename = perms.get(self.action, 'coves.view')
|
||||
return [IsAuthenticated(), require_permission(codename)()]
|
||||
@@ -2668,6 +2783,110 @@ class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete()
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='reset-cove')
|
||||
def reset_cove(self, request, pk=None):
|
||||
"""
|
||||
Detecta inconsistencia cuando la COVE está marcada como descargada pero el
|
||||
documento no existe en BD o el archivo falta en storage. Crea error tipo 20
|
||||
para Errores VU y restablece cove_estado='pendiente' con contador en 0.
|
||||
"""
|
||||
cove = self.get_object()
|
||||
|
||||
if cove.cove_estado != EstadoDescarga.DESCARGADO:
|
||||
return Response(
|
||||
{"error": "La COVE no está marcada como descargada"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Documentos generales de la COVE: se excluyen acuse (7), requests (19, 23)
|
||||
# y errores (20, 24) del catálogo document_type
|
||||
cove_docs = Document.objects.filter(
|
||||
pedimento=cove.pedimento,
|
||||
archivo__icontains=cove.numero_cove,
|
||||
).exclude(document_type_id__in=[7, 19, 20, 23, 24])
|
||||
cove_disponible = any(
|
||||
doc.size and storage_service.file_exists(doc.archivo.name)
|
||||
for doc in cove_docs
|
||||
)
|
||||
|
||||
if cove_disponible:
|
||||
return Response(
|
||||
{"status": "La COVE está disponible correctamente", "cove_disponible": True},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
_crear_documento_error_vu(
|
||||
registro=cove,
|
||||
numero=cove.numero_cove,
|
||||
doc_type_error_id=20,
|
||||
mensaje=(
|
||||
f"Inconsistencia detectada: la COVE {cove.numero_cove} fue marcada "
|
||||
f"como descargada pero el documento no se encuentra disponible. "
|
||||
f"El estado fue restablecido para permitir reprocesamiento."
|
||||
),
|
||||
file_prefix='error_cove',
|
||||
)
|
||||
|
||||
cove.cove_estado = EstadoDescarga.PENDIENTE
|
||||
cove.cove_intentos = 0
|
||||
cove.ultimo_error = None
|
||||
cove.save()
|
||||
|
||||
serializer = self.get_serializer(cove)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='reset-acuse-cove')
|
||||
def reset_acuse_cove(self, request, pk=None):
|
||||
"""
|
||||
Detecta inconsistencia cuando el acuse de la COVE (tipo 7) está marcado como
|
||||
descargado pero no existe en BD o el archivo falta en storage. Crea error
|
||||
tipo 24 para Errores VU y restablece acuse_cove_estado='pendiente' con
|
||||
contador en 0.
|
||||
"""
|
||||
cove = self.get_object()
|
||||
|
||||
if cove.acuse_cove_estado != EstadoDescarga.DESCARGADO:
|
||||
return Response(
|
||||
{"error": "El acuse de la COVE no está marcado como descargado"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
acuse_docs = Document.objects.filter(
|
||||
pedimento=cove.pedimento,
|
||||
archivo__icontains=cove.numero_cove,
|
||||
document_type_id=7
|
||||
)
|
||||
acuse_disponible = any(
|
||||
doc.size and storage_service.file_exists(doc.archivo.name)
|
||||
for doc in acuse_docs
|
||||
)
|
||||
|
||||
if acuse_disponible:
|
||||
return Response(
|
||||
{"status": "El acuse está disponible correctamente", "acuse_disponible": True},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
_crear_documento_error_vu(
|
||||
registro=cove,
|
||||
numero=cove.numero_cove,
|
||||
doc_type_error_id=24,
|
||||
mensaje=(
|
||||
f"Inconsistencia detectada: el acuse de la COVE {cove.numero_cove} "
|
||||
f"fue marcado como descargado pero el documento no se encuentra disponible. "
|
||||
f"El estado fue restablecido para permitir reprocesamiento."
|
||||
),
|
||||
file_prefix='error_acuse_cove',
|
||||
)
|
||||
|
||||
cove.acuse_cove_estado = EstadoDescarga.PENDIENTE
|
||||
cove.acuse_cove_intentos = 0
|
||||
cove.ultimo_error = None
|
||||
cove.save()
|
||||
|
||||
serializer = self.get_serializer(cove)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
class ImportadorViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for Importador model.
|
||||
|
||||
Reference in New Issue
Block a user