# Referencia Completa — Django Experto

## Django REST Framework — Serializers con Validación

```python
from rest_framework import serializers
from .models import Factura, ItemFactura

class ItemFacturaSerializer(serializers.ModelSerializer):
    class Meta:
        model = ItemFactura
        fields = ["id", "producto_id", "cantidad", "precio_unitario", "subtotal"]
        read_only_fields = ["subtotal"]

class FacturaSerializer(serializers.ModelSerializer):
    items = ItemFacturaSerializer(many=True)
    nombre_cliente = serializers.CharField(
        source="cliente.nombre", read_only=True
    )
    total = serializers.DecimalField(
        max_digits=12, decimal_places=2, read_only=True,
        source="calcular_total"
    )

    class Meta:
        model = Factura
        fields = [
            "id", "folio", "fecha", "estatus",
            "nombre_cliente", "items", "total"
        ]
        read_only_fields = ["id", "folio"]

    def validate_fecha(self, value):
        from datetime import date
        if value < date.today():
            raise serializers.ValidationError(
                "La fecha no puede ser anterior a hoy."
            )
        return value

    def validate(self, attrs):
        if not attrs.get("items"):
            raise serializers.ValidationError(
                {"items": "La factura debe tener al menos un ítem."}
            )
        return attrs

    def create(self, validated_data):
        items_data = validated_data.pop("items")
        factura = Factura.objects.create(**validated_data)
        for item_data in items_data:
            ItemFactura.objects.create(factura=factura, **item_data)
        return factura
```

---

## ViewSets con Permisos

```python
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend

class FacturaViewSet(viewsets.ModelViewSet):
    serializer_class = FacturaSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ["estatus", "fecha"]

    def get_queryset(self):
        # IDOR: siempre filtrar por la empresa del usuario autenticado
        return (
            Factura.objects
            .filter(empresa=self.request.user.empresa)
            .select_related("cliente")
            .prefetch_related("items__producto")
            .order_by("-fecha")
        )

    def perform_create(self, serializer):
        serializer.save(
            empresa=self.request.user.empresa,
            creado_por=self.request.user.email,
        )

    @action(detail=True, methods=["post"], url_path="cancelar")
    def cancelar(self, request, pk=None):
        factura = self.get_object()
        if factura.estatus == "cancelada":
            return Response(
                {"detail": "La factura ya está cancelada."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        factura.cancelar(usuario=request.user)
        return Response(self.get_serializer(factura).data)
```

---

## Middleware

```python
# common/middleware.py
import time
import logging

logger = logging.getLogger(__name__)

class TiempoRespuestaMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        inicio = time.monotonic()
        response = self.get_response(request)
        duracion = time.monotonic() - inicio
        response["X-Response-Time"] = f"{duracion:.4f}s"
        if duracion > 2.0:
            logger.warning(
                f"Respuesta lenta: {request.method} {request.path} "
                f"took {duracion:.2f}s"
            )
        return response
```

---

## Testing Django con pytest

```python
# apps/facturas/tests/test_api.py
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from apps.facturas.tests.factories import FacturaFactory, UsuarioFactory

@pytest.fixture
def api_client():
    return APIClient()

@pytest.fixture
def usuario_autenticado(api_client):
    usuario = UsuarioFactory()
    api_client.force_authenticate(user=usuario)
    return usuario, api_client

@pytest.mark.django_db
def test_crear_factura(usuario_autenticado):
    usuario, client = usuario_autenticado
    url = reverse("factura-list")
    datos = {
        "fecha": "2026-03-25",
        "cliente_id": str(usuario.empresa.clientes.first().id),
        "items": [{"producto_id": "...", "cantidad": 2, "precio_unitario": "100.00"}],
    }
    respuesta = client.post(url, datos, format="json")
    assert respuesta.status_code == 201
    assert respuesta.data["estatus"] == "borrador"

@pytest.mark.django_db
def test_no_puede_ver_facturas_de_otra_empresa(usuario_autenticado):
    usuario, client = usuario_autenticado
    factura_ajena = FacturaFactory()  # Pertenece a otra empresa
    url = reverse("factura-detail", kwargs={"pk": factura_ajena.id})
    respuesta = client.get(url)
    assert respuesta.status_code == 404  # No 403, para no revelar existencia
```

### Factory Boy para Fixtures

```python
# apps/facturas/tests/factories.py
import factory
from factory.django import DjangoModelFactory
from apps.facturas.models import Factura, ItemFactura

class FacturaFactory(DjangoModelFactory):
    class Meta:
        model = Factura

    folio = factory.Sequence(lambda n: f"F-{n:05d}")
    fecha = factory.Faker("date_this_year")
    estatus = "borrador"
    empresa = factory.SubFactory("apps.empresas.tests.factories.EmpresaFactory")
    cliente = factory.SubFactory("apps.clientes.tests.factories.ClienteFactory")
```
