Files
backend/api/customs/serializers.py
marcos 244bbcb21c fix: filtrado de partidas por nomenclatura de documento (core/partida_docs)
Frontera (_|.|$) tras vu_PT_{app}_{numero} para cubrir los 3 formatos sin
confundir partida 1 con 11/100. Fuente unica en core/partida_docs.py, reusada
por get_documentos, handlers de borrado/descarga y fix_partidas_error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 07:31:40 -06:00

304 lines
13 KiB
Python

from rest_framework import serializers
from api.customs.models import (
Pedimento,
TipoOperacion,
ProcesamientoPedimento,
EDocument,
Cove,
Importador,
Partida,
EstadoDescarga
)
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.serializers import DocumentSerializer
from api.vucem.serializers import VucemSerializer
from core.partida_docs import es_doc_de_partida
import logging
logger = logging.getLogger(__name__)
class PedimentoSerializer(serializers.ModelSerializer):
documentos_count = serializers.SerializerMethodField()
documentos_peso_total = serializers.SerializerMethodField()
def get_documentos_count(self, obj):
# Si obj es un dict o no tiene 'documents', devuelve 0
if isinstance(obj, dict) or not hasattr(obj, 'documents'):
return 0
return obj.documents.count()
def get_documentos_peso_total(self, obj):
# Si obj es un dict o no tiene 'documents', devuelve 0
if isinstance(obj, dict) or not hasattr(obj, 'documents'):
return 0
return obj.documents.aggregate(total=models.Sum('size'))['total'] or 0
class Meta:
model = Pedimento
fields = '__all__'
read_only_fields = (
'created_at', 'updated_at', 'organizacion', 'pedimento_app',
'documentos_count', 'documentos_peso_total'
)
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['documentos_count'] = self.get_documentos_count(instance)
rep['documentos_peso_total'] = self.get_documentos_peso_total(instance)
return rep
class PartidaSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
def get_documentos(self, obj):
if not obj or not getattr(obj, 'pedimento', None) or not getattr(obj, 'numero_partida', None):
return []
try:
# El matching documento→partida se hace por nombre de archivo con
# frontera real (core.partida_docs); document_type_id=1 son los
# documentos de respuesta de partida (excluye REQUEST/ERROR 17/18).
mapa = self.context.get('docs_por_partida')
if mapa is not None:
# Camino optimizado: la vista precargó el mapa de la página.
docs = mapa.get((obj.pedimento_id, obj.numero_partida), [])
else:
# Fallback (retrieve u otros callers): una consulta por partida.
qs = Document.objects.filter(
pedimento=obj.pedimento,
document_type_id=1,
).select_related('pedimento') # evita N+1 en DocumentSerializer.get_pedimento_numero
app = obj.pedimento.pedimento_app
docs = [d for d in qs if es_doc_de_partida(d.archivo.name, app, obj.numero_partida)]
org_id = getattr(obj, 'organizacion_id', None)
if org_id:
docs = [d for d in docs if d.organizacion_id == org_id]
return DocumentSerializer(docs, many=True, context=self.context).data
except Exception as e:
logger.warning("get_documentos partida %s: %s", getattr(obj, 'id', '?'), e)
return []
class Meta:
model = Partida
fields = '__all__'
read_only_fields = ('created_at', 'updated_at', 'numero_partida')
def validate_unique(self, attrs):
"""
Sobrescribe la validación de unicidad para manejar correctamente
las actualizaciones de registros existentes.
"""
# Si estamos actualizando un registro existente, excluirlo de la validación
if self.instance:
# Para actualizaciones, crear una instancia temporal con los nuevos datos
# pero sin guardarla, solo para validar unicidad excluyendo el registro actual
exclude = {'id': self.instance.id}
else:
# Para creaciones nuevas, no excluir nada
exclude = {}
# Crear una instancia temporal con los datos combinados
if self.instance:
# Combinar datos existentes con los nuevos
combined_attrs = {}
for field in self.Meta.model._meta.fields:
field_name = field.name
if field_name in attrs:
combined_attrs[field_name] = attrs[field_name]
elif hasattr(self.instance, field_name):
combined_attrs[field_name] = getattr(self.instance, field_name)
else:
combined_attrs = attrs
# Verificar unique_together manualmente para pedimento + numero_partida
if 'pedimento' in combined_attrs and 'numero_partida' in combined_attrs:
queryset = self.Meta.model.objects.filter(
pedimento=combined_attrs['pedimento'],
numero_partida=combined_attrs['numero_partida']
)
# Si estamos actualizando, excluir el registro actual
if self.instance:
queryset = queryset.exclude(id=self.instance.id)
if queryset.exists():
raise serializers.ValidationError({
'non_field_errors': [
f'Ya existe una partida con el número {combined_attrs["numero_partida"]} para este pedimento.'
]
})
def validate(self, data):
"""
Validación adicional personalizada.
"""
# Llamar a la validación de unicidad personalizada
self.validate_unique(data)
return data
class TipoOperacionSerializer(serializers.ModelSerializer):
class Meta:
model = TipoOperacion
fields = '__all__'
class ProcesamientoPedimentoSerializer(serializers.ModelSerializer):
organizacion = serializers.PrimaryKeyRelatedField(queryset=ProcesamientoPedimento._meta.get_field('organizacion').related_model.objects.all(), required=False)
organizacion_name = serializers.CharField(source='organizacion.nombre', read_only=True)
class Meta:
model = ProcesamientoPedimento
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request = self.context.get('request')
# Si no es superusuario, hacer organizacion read_only
if request and hasattr(request, 'user') and not request.user.is_superuser:
self.fields['organizacion'].read_only = True
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['pedimento'] = PedimentoSerializer(instance.pedimento).data
return representation
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()
# excluir solo request (21, 25); errores (22, 26) se incluyen para detección en frontend
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
).exclude(document_type_id__in=[21, 25])
# 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:
model = EDocument
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
def validate(self, attrs):
# Compatibilidad: payloads legados que solo mandan los booleanos se traducen
# al estado de 3 valores (fuente de verdad en el modelo). Un False legado no
# degrada un estado 'error' ya asignado.
if 'edocument_descargado' in attrs and 'edocument_estado' not in attrs:
if attrs['edocument_descargado']:
attrs['edocument_estado'] = EstadoDescarga.DESCARGADO
elif not (self.instance and self.instance.edocument_estado == EstadoDescarga.ERROR):
attrs['edocument_estado'] = EstadoDescarga.PENDIENTE
if 'acuse_descargado' in attrs and 'acuse_estado' not in attrs:
if attrs['acuse_descargado']:
attrs['acuse_estado'] = EstadoDescarga.DESCARGADO
elif not (self.instance and self.instance.acuse_estado == EstadoDescarga.ERROR):
attrs['acuse_estado'] = EstadoDescarga.PENDIENTE
return attrs
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Si no es superusuario, hacer organizacion read_only
request = self.context.get('request')
if request and hasattr(request, 'user') and not request.user.is_superuser:
self.fields['organizacion'].read_only = True
class CoveSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
class Meta:
model = Cove
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
def validate(self, attrs):
# Compatibilidad: payloads legados que solo mandan los booleanos se traducen
# al estado de 3 valores (fuente de verdad en el modelo). Un False legado no
# degrada un estado 'error' ya asignado.
if 'cove_descargado' in attrs and 'cove_estado' not in attrs:
if attrs['cove_descargado']:
attrs['cove_estado'] = EstadoDescarga.DESCARGADO
elif not (self.instance and self.instance.cove_estado == EstadoDescarga.ERROR):
attrs['cove_estado'] = EstadoDescarga.PENDIENTE
if 'acuse_cove_descargado' in attrs and 'acuse_cove_estado' not in attrs:
if attrs['acuse_cove_descargado']:
attrs['acuse_cove_estado'] = EstadoDescarga.DESCARGADO
elif not (self.instance and self.instance.acuse_cove_estado == EstadoDescarga.ERROR):
attrs['acuse_cove_estado'] = EstadoDescarga.PENDIENTE
return attrs
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()
# Excluir solo request (19, 23); errores (20, 24) se incluyen para detección en frontend
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
).exclude(document_type_id__in=[19, 23])
# 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 Meta:
model = Importador
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')